1#   Copyright 2016 - The Android Open Source Project
2#
3#   Licensed under the Apache License, Version 2.0 (the "License");
4#   you may not use this file except in compliance with the License.
5#   You may obtain a copy of the License at
6#
7#       http://www.apache.org/licenses/LICENSE-2.0
8#
9#   Unless required by applicable law or agreed to in writing, software
10#   distributed under the License is distributed on an "AS IS" BASIS,
11#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12#   See the License for the specific language governing permissions and
13#   limitations under the License.
14
15import enum
16import logging
17import os
18import collections
19import itertools
20
21from acts.controllers.ap_lib import hostapd_constants
22
23
24def ht40_plus_allowed(channel):
25    """Returns: True iff HT40+ is enabled for this configuration."""
26    channel_supported = (channel in hostapd_constants.HT40_ALLOW_MAP[
27        hostapd_constants.N_CAPABILITY_HT40_PLUS_CHANNELS])
28    return (channel_supported)
29
30
31def ht40_minus_allowed(channel):
32    """Returns: True iff HT40- is enabled for this configuration."""
33    channel_supported = (channel in hostapd_constants.HT40_ALLOW_MAP[
34        hostapd_constants.N_CAPABILITY_HT40_MINUS_CHANNELS])
35    return (channel_supported)
36
37
38def get_frequency_for_channel(channel):
39    """The frequency associated with a given channel number.
40
41    Args:
42        value: int channel number.
43
44    Returns:
45        int, frequency in MHz associated with the channel.
46
47    """
48    for frequency, channel_iter in \
49        hostapd_constants.CHANNEL_MAP.items():
50        if channel == channel_iter:
51            return frequency
52    else:
53        raise ValueError('Unknown channel value: %r.' % channel)
54
55
56def get_channel_for_frequency(frequency):
57    """The channel number associated with a given frequency.
58
59    Args:
60        value: int frequency in MHz.
61
62    Returns:
63        int, frequency associated with the channel.
64
65    """
66    return hostapd_constants.CHANNEL_MAP[frequency]
67
68
69class HostapdConfig(object):
70    """The root settings for the router.
71
72    All the settings for a router that are not part of an ssid.
73    """
74    def _get_11ac_center_channel_from_channel(self, channel):
75        """Returns the center channel of the selected channel band based
76           on the channel and channel bandwidth provided.
77        """
78        channel = int(channel)
79        center_channel_delta = hostapd_constants.CENTER_CHANNEL_MAP[
80            self._vht_oper_chwidth]['delta']
81
82        for channel_map in hostapd_constants.CENTER_CHANNEL_MAP[
83                self._vht_oper_chwidth]['channels']:
84            lower_channel_bound, upper_channel_bound = channel_map
85            if lower_channel_bound <= channel <= upper_channel_bound:
86                return lower_channel_bound + center_channel_delta
87        raise ValueError('Invalid channel for {channel_width}.'.format(
88            channel_width=self._vht_oper_chwidth))
89
90    @property
91    def _get_default_config(self):
92        """Returns: dict of default options for hostapd."""
93        if self.set_ap_defaults_profile == 'mistral':
94            return collections.OrderedDict([
95                ('logger_syslog', '-1'),
96                ('logger_syslog_level', '0'),
97                # default RTS and frag threshold to ``off''
98                ('rts_threshold', None),
99                ('fragm_threshold', None),
100                ('driver', hostapd_constants.DRIVER_NAME)
101            ])
102        else:
103            return collections.OrderedDict([
104                ('logger_syslog', '-1'),
105                ('logger_syslog_level', '0'),
106                # default RTS and frag threshold to ``off''
107                ('rts_threshold', '2347'),
108                ('fragm_threshold', '2346'),
109                ('driver', hostapd_constants.DRIVER_NAME)
110            ])
111
112    @property
113    def _hostapd_ht_capabilities(self):
114        """Returns: string suitable for the ht_capab= line in a hostapd config.
115        """
116        ret = []
117        for cap in hostapd_constants.N_CAPABILITIES_MAPPING.keys():
118            if cap in self._n_capabilities:
119                ret.append(hostapd_constants.N_CAPABILITIES_MAPPING[cap])
120        return ''.join(ret)
121
122    @property
123    def _hostapd_vht_capabilities(self):
124        """Returns: string suitable for the vht_capab= line in a hostapd config.
125        """
126        ret = []
127        for cap in hostapd_constants.AC_CAPABILITIES_MAPPING.keys():
128            if cap in self._ac_capabilities:
129                ret.append(hostapd_constants.AC_CAPABILITIES_MAPPING[cap])
130        return ''.join(ret)
131
132    @property
133    def _require_ht(self):
134        """Returns: True iff clients should be required to support HT."""
135        # TODO(wiley) Why? (crbug.com/237370)
136        # DOES THIS APPLY TO US?
137        logging.warning('Not enforcing pure N mode because Snow does '
138                        'not seem to support it...')
139        return False
140
141    @property
142    def _require_vht(self):
143        """Returns: True if clients should be required to support VHT."""
144        return self._mode == hostapd_constants.MODE_11AC_PURE
145
146    @property
147    def hw_mode(self):
148        """Returns: string hardware mode understood by hostapd."""
149        if self._mode == hostapd_constants.MODE_11A:
150            return hostapd_constants.MODE_11A
151        if self._mode == hostapd_constants.MODE_11B:
152            return hostapd_constants.MODE_11B
153        if self._mode == hostapd_constants.MODE_11G:
154            return hostapd_constants.MODE_11G
155        if self.is_11n or self.is_11ac:
156            # For their own historical reasons, hostapd wants it this way.
157            if self._frequency > 5000:
158                return hostapd_constants.MODE_11A
159            return hostapd_constants.MODE_11G
160        raise ValueError('Invalid mode.')
161
162    @property
163    def is_11n(self):
164        """Returns: True if we're trying to host an 802.11n network."""
165        return self._mode in (hostapd_constants.MODE_11N_MIXED,
166                              hostapd_constants.MODE_11N_PURE)
167
168    @property
169    def is_11ac(self):
170        """Returns: True if we're trying to host an 802.11ac network."""
171        return self._mode in (hostapd_constants.MODE_11AC_MIXED,
172                              hostapd_constants.MODE_11AC_PURE)
173
174    @property
175    def channel(self):
176        """Returns: int channel number for self.frequency."""
177        return get_channel_for_frequency(self.frequency)
178
179    @channel.setter
180    def channel(self, value):
181        """Sets the channel number to configure hostapd to listen on.
182
183        Args:
184            value: int, channel number.
185
186        """
187        self.frequency = get_frequency_for_channel(value)
188
189    @property
190    def bssid(self):
191        return self._bssid
192
193    @bssid.setter
194    def bssid(self, value):
195        self._bssid = value
196
197    @property
198    def frequency(self):
199        """Returns: int, frequency for hostapd to listen on."""
200        return self._frequency
201
202    @frequency.setter
203    def frequency(self, value):
204        """Sets the frequency for hostapd to listen on.
205
206        Args:
207            value: int, frequency in MHz.
208
209        """
210        if value not in hostapd_constants.CHANNEL_MAP:
211            raise ValueError('Tried to set an invalid frequency: %r.' % value)
212
213        self._frequency = value
214
215    @property
216    def bss_lookup(self):
217        return self._bss_lookup
218
219    @property
220    def ssid(self):
221        """Returns: SsidSettings, The root Ssid settings being used."""
222        return self._ssid
223
224    @ssid.setter
225    def ssid(self, value):
226        """Sets the ssid for the hostapd.
227
228        Args:
229            value: SsidSettings, new ssid settings to use.
230
231        """
232        self._ssid = value
233
234    @property
235    def hidden(self):
236        """Returns: bool, True if the ssid is hidden, false otherwise."""
237        return self._hidden
238
239    @hidden.setter
240    def hidden(self, value):
241        """Sets if this ssid is hidden.
242
243        Args:
244            value: bool, If true the ssid will be hidden.
245        """
246        self.hidden = value
247
248    @property
249    def security(self):
250        """Returns: The security type being used."""
251        return self._security
252
253    @security.setter
254    def security(self, value):
255        """Sets the security options to use.
256
257        Args:
258            value: Security, The type of security to use.
259        """
260        self._security = value
261
262    @property
263    def ht_packet_capture_mode(self):
264        """Get an appropriate packet capture HT parameter.
265
266        When we go to configure a raw monitor we need to configure
267        the phy to listen on the correct channel.  Part of doing
268        so is to specify the channel width for HT channels.  In the
269        case that the AP is configured to be either HT40+ or HT40-,
270        we could return the wrong parameter because we don't know which
271        configuration will be chosen by hostap.
272
273        Returns:
274            string, HT parameter for frequency configuration.
275
276        """
277        if not self.is_11n:
278            return None
279
280        if ht40_plus_allowed(self.channel):
281            return 'HT40+'
282
283        if ht40_minus_allowed(self.channel):
284            return 'HT40-'
285
286        return 'HT20'
287
288    @property
289    def beacon_footer(self):
290        """Returns: bool _beacon_footer value."""
291        return self._beacon_footer
292
293    def beacon_footer(self, value):
294        """Changes the beacon footer.
295
296        Args:
297            value: bool, The beacon footer vlaue.
298        """
299        self._beacon_footer = value
300
301    @property
302    def scenario_name(self):
303        """Returns: string _scenario_name value, or None."""
304        return self._scenario_name
305
306    @property
307    def min_streams(self):
308        """Returns: int, _min_streams value, or None."""
309        return self._min_streams
310
311    def __init__(self,
312                 interface=None,
313                 mode=None,
314                 channel=None,
315                 frequency=None,
316                 n_capabilities=[],
317                 beacon_interval=None,
318                 dtim_period=None,
319                 frag_threshold=None,
320                 rts_threshold=None,
321                 short_preamble=None,
322                 ssid=None,
323                 hidden=False,
324                 security=None,
325                 bssid=None,
326                 force_wmm=None,
327                 pmf_support=None,
328                 obss_interval=None,
329                 vht_channel_width=None,
330                 vht_center_channel=None,
331                 ac_capabilities=[],
332                 beacon_footer='',
333                 spectrum_mgmt_required=None,
334                 scenario_name=None,
335                 min_streams=None,
336                 bss_settings=[],
337                 additional_parameters={},
338                 set_ap_defaults_profile='whirlwind'):
339        """Construct a HostapdConfig.
340
341        You may specify channel or frequency, but not both.  Both options
342        are checked for validity (i.e. you can't specify an invalid channel
343        or a frequency that will not be accepted).
344
345        Args:
346            interface: string, The name of the interface to use.
347            mode: string, MODE_11x defined above.
348            channel: int, channel number.
349            frequency: int, frequency of channel.
350            n_capabilities: list of N_CAPABILITY_x defined above.
351            beacon_interval: int, beacon interval of AP.
352            dtim_period: int, include a DTIM every |dtim_period| beacons.
353            frag_threshold: int, maximum outgoing data frame size.
354            rts_threshold: int, maximum packet size without requiring explicit
355                protection via rts/cts or cts to self.
356            short_preamble: Whether to use a short preamble.
357            ssid: string, The name of the ssid to brodcast.
358            hidden: bool, Should the ssid be hidden.
359            security: Security, the secuirty settings to use.
360            bssid: string, a MAC address like string for the BSSID.
361            force_wmm: True if we should force WMM on, False if we should
362                force it off, None if we shouldn't force anything.
363            pmf_support: one of PMF_SUPPORT_* above.  Controls whether the
364                client supports/must support 802.11w. If None, defaults to
365                required with wpa3, else defaults to disabled.
366            obss_interval: int, interval in seconds that client should be
367                required to do background scans for overlapping BSSes.
368            vht_channel_width: object channel width
369            vht_center_channel: int, center channel of segment 0.
370            ac_capabilities: list of AC_CAPABILITY_x defined above.
371            beacon_footer: string, containing (unvalidated) IE data to be
372                placed at the end of the beacon.
373            spectrum_mgmt_required: True if we require the DUT to support
374                spectrum management.
375            scenario_name: string to be included in file names, instead
376                of the interface name.
377            min_streams: int, number of spatial streams required.
378            control_interface: The file name to use as the control interface.
379            bss_settings: The settings for all bss.
380            additional_parameters: A dictionary of additional parameters to add
381                to the hostapd config.
382            set_ap_defaults_profile: profile name to load defaults from
383        """
384        self.set_ap_defaults_profile = set_ap_defaults_profile
385        self._interface = interface
386        if channel is not None and frequency is not None:
387            raise ValueError('Specify either frequency or channel '
388                             'but not both.')
389
390        self._wmm_enabled = False
391        unknown_caps = [
392            cap for cap in n_capabilities
393            if cap not in hostapd_constants.N_CAPABILITIES_MAPPING
394        ]
395        if unknown_caps:
396            raise ValueError('Unknown capabilities: %r' % unknown_caps)
397
398        self._frequency = None
399        if channel:
400            self.channel = channel
401        elif frequency:
402            self.frequency = frequency
403        else:
404            raise ValueError('Specify either frequency or channel.')
405        '''
406        if set_ap_defaults_model:
407            ap_default_config = hostapd_ap_default_configs.APDefaultConfig(
408                profile_name=set_ap_defaults_model, frequency=self.frequency)
409            force_wmm = ap_default_config.force_wmm
410            beacon_interval = ap_default_config.beacon_interval
411            dtim_period = ap_default_config.dtim_period
412            short_preamble = ap_default_config.short_preamble
413            self._interface = ap_default_config.interface
414            mode = ap_default_config.mode
415            if ap_default_config.n_capabilities:
416                n_capabilities = ap_default_config.n_capabilities
417            if ap_default_config.ac_capabilities:
418                ap_default_config = ap_default_config.ac_capabilities
419        '''
420
421        self._n_capabilities = set(n_capabilities)
422        if self._n_capabilities:
423            self._wmm_enabled = True
424        if self._n_capabilities and mode is None:
425            mode = hostapd_constants.MODE_11N_PURE
426        self._mode = mode
427
428        if not self.supports_frequency(self.frequency):
429            raise ValueError('Configured a mode %s that does not support '
430                             'frequency %d' % (self._mode, self.frequency))
431
432        self._beacon_interval = beacon_interval
433        self._dtim_period = dtim_period
434        self._frag_threshold = frag_threshold
435        self._rts_threshold = rts_threshold
436        self._short_preamble = short_preamble
437        self._ssid = ssid
438        self._hidden = hidden
439        self._security = security
440        self._bssid = bssid
441        if force_wmm is not None:
442            if force_wmm:
443                self._wmm_enabled = 1
444            else:
445                self._wmm_enabled = 0
446        if pmf_support is None:
447            if self.security and self.security.wpa3:
448                self._pmf_support = hostapd_constants.PMF_SUPPORT_REQUIRED
449            else:
450                self._pmf_support = hostapd_constants.PMF_SUPPORT_DISABLED
451        elif pmf_support not in hostapd_constants.PMF_SUPPORT_VALUES:
452            raise ValueError('Invalid value for pmf_support: %r' % pmf_support)
453        elif (pmf_support != hostapd_constants.PMF_SUPPORT_REQUIRED
454              and self.security and self.security.wpa3):
455            raise ValueError('PMF support must be required with wpa3.')
456        else:
457            self._pmf_support = pmf_support
458        self._obss_interval = obss_interval
459        if self.is_11ac:
460            if str(vht_channel_width) == '40' or str(
461                    vht_channel_width) == '20':
462                self._vht_oper_chwidth = hostapd_constants.VHT_CHANNEL_WIDTH_40
463            elif str(vht_channel_width) == '80':
464                self._vht_oper_chwidth = hostapd_constants.VHT_CHANNEL_WIDTH_80
465            elif str(vht_channel_width) == '160':
466                self._vht_oper_chwidth = hostapd_constants.VHT_CHANNEL_WIDTH_160
467            elif str(vht_channel_width) == '80+80':
468                self._vht_oper_chwidth = hostapd_constants.VHT_CHANNEL_WIDTH_80_80
469            elif vht_channel_width is not None:
470                raise ValueError('Invalid channel width')
471            else:
472                logging.warning(
473                    'No channel bandwidth specified.  Using 80MHz for 11ac.')
474                self._vht_oper_chwidth = 1
475            if vht_center_channel is not None:
476                self._vht_oper_centr_freq_seg0_idx = vht_center_channel
477            elif vht_channel_width == 20:
478                self._vht_oper_centr_freq_seg0_idx = channel
479            else:
480                self._vht_oper_centr_freq_seg0_idx = self._get_11ac_center_channel_from_channel(
481                    self.channel)
482            self._ac_capabilities = set(ac_capabilities)
483        self._beacon_footer = beacon_footer
484        self._spectrum_mgmt_required = spectrum_mgmt_required
485        self._scenario_name = scenario_name
486        self._min_streams = min_streams
487        self._additional_parameters = additional_parameters
488
489        self._bss_lookup = collections.OrderedDict()
490        for bss in bss_settings:
491            if bss.name in self._bss_lookup:
492                raise ValueError('Cannot have multiple bss settings with the'
493                                 ' same name.')
494            self._bss_lookup[bss.name] = bss
495
496    def __repr__(self):
497        return (
498            '%s(mode=%r, channel=%r, frequency=%r, '
499            'n_capabilities=%r, beacon_interval=%r, '
500            'dtim_period=%r, frag_threshold=%r, ssid=%r, bssid=%r, '
501            'wmm_enabled=%r, security_config=%r, '
502            'spectrum_mgmt_required=%r)' %
503            (self.__class__.__name__, self._mode, self.channel, self.frequency,
504             self._n_capabilities, self._beacon_interval, self._dtim_period,
505             self._frag_threshold, self._ssid, self._bssid, self._wmm_enabled,
506             self._security, self._spectrum_mgmt_required))
507
508    def supports_channel(self, value):
509        """Check whether channel is supported by the current hardware mode.
510
511        @param value: int channel to check.
512        @return True iff the current mode supports the band of the channel.
513
514        """
515        for freq, channel in hostapd_constants.CHANNEL_MAP.iteritems():
516            if channel == value:
517                return self.supports_frequency(freq)
518
519        return False
520
521    def supports_frequency(self, frequency):
522        """Check whether frequency is supported by the current hardware mode.
523
524        @param frequency: int frequency to check.
525        @return True iff the current mode supports the band of the frequency.
526
527        """
528        if self._mode == hostapd_constants.MODE_11A and frequency < 5000:
529            return False
530
531        if self._mode in (hostapd_constants.MODE_11B,
532                          hostapd_constants.MODE_11G) and frequency > 5000:
533            return False
534
535        if frequency not in hostapd_constants.CHANNEL_MAP:
536            return False
537
538        channel = hostapd_constants.CHANNEL_MAP[frequency]
539        supports_plus = (channel in hostapd_constants.HT40_ALLOW_MAP[
540            hostapd_constants.N_CAPABILITY_HT40_PLUS_CHANNELS])
541        supports_minus = (channel in hostapd_constants.HT40_ALLOW_MAP[
542            hostapd_constants.N_CAPABILITY_HT40_MINUS_CHANNELS])
543        if (hostapd_constants.N_CAPABILITY_HT40_PLUS in self._n_capabilities
544                and not supports_plus):
545            return False
546
547        if (hostapd_constants.N_CAPABILITY_HT40_MINUS in self._n_capabilities
548                and not supports_minus):
549            return False
550
551        return True
552
553    def add_bss(self, bss):
554        """Adds a new bss setting.
555
556        Args:
557            bss: The bss settings to add.
558        """
559        if bss.name in self._bss_lookup:
560            raise ValueError('A bss with the same name already exists.')
561
562        self._bss_lookup[bss.name] = bss
563
564    def remove_bss(self, bss_name):
565        """Removes a bss setting from the config."""
566        del self._bss_lookup[bss_name]
567
568    def package_configs(self):
569        """Package the configs.
570
571        Returns:
572            A list of dictionaries, one dictionary for each section of the
573            config.
574        """
575        # Start with the default config parameters.
576        conf = self._get_default_config
577
578        if self._interface:
579            conf['interface'] = self._interface
580        if self._bssid:
581            conf['bssid'] = self._bssid
582        if self._ssid:
583            conf['ssid'] = self._ssid
584            conf['ignore_broadcast_ssid'] = 1 if self._hidden else 0
585        conf['channel'] = self.channel
586        conf['hw_mode'] = self.hw_mode
587        if self.is_11n or self.is_11ac:
588            conf['ieee80211n'] = 1
589            conf['ht_capab'] = self._hostapd_ht_capabilities
590        if self.is_11ac:
591            conf['ieee80211ac'] = 1
592            conf['vht_oper_chwidth'] = self._vht_oper_chwidth
593            conf['vht_oper_centr_freq_seg0_idx'] = \
594                    self._vht_oper_centr_freq_seg0_idx
595            conf['vht_capab'] = self._hostapd_vht_capabilities
596        if self._wmm_enabled is not None:
597            conf['wmm_enabled'] = self._wmm_enabled
598        if self._require_ht:
599            conf['require_ht'] = 1
600        if self._require_vht:
601            conf['require_vht'] = 1
602        if self._beacon_interval:
603            conf['beacon_int'] = self._beacon_interval
604        if self._dtim_period:
605            conf['dtim_period'] = self._dtim_period
606        if self._frag_threshold:
607            conf['fragm_threshold'] = self._frag_threshold
608        if self._rts_threshold:
609            conf['rts_threshold'] = self._rts_threshold
610        if self._pmf_support:
611            conf['ieee80211w'] = self._pmf_support
612        if self._obss_interval:
613            conf['obss_interval'] = self._obss_interval
614        if self._short_preamble:
615            conf['preamble'] = 1
616        if self._spectrum_mgmt_required:
617            # To set spectrum_mgmt_required, we must first set
618            # local_pwr_constraint. And to set local_pwr_constraint,
619            # we must first set ieee80211d. And to set ieee80211d, ...
620            # Point being: order matters here.
621            conf['country_code'] = 'US'  # Required for local_pwr_constraint
622            conf['ieee80211d'] = 1  # Required for local_pwr_constraint
623            conf['local_pwr_constraint'] = 0  # No local constraint
624            conf['spectrum_mgmt_required'] = 1  # Requires local_pwr_constraint
625
626        if self._security:
627            for k, v in self._security.generate_dict().items():
628                conf[k] = v
629
630        all_conf = [conf]
631
632        for bss in self._bss_lookup.values():
633            bss_conf = collections.OrderedDict()
634            for k, v in (bss.generate_dict()).items():
635                bss_conf[k] = v
636            all_conf.append(bss_conf)
637
638        if self._additional_parameters:
639            all_conf.append(self._additional_parameters)
640
641        return all_conf
642