| # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import logging |
| |
| from autotest_lib.client.common_lib import error |
| |
| PYSHARK_LOAD_TIMEOUT = 2 |
| FRAME_FIELD_RADIOTAP_DATARATE = 'radiotap.datarate' |
| FRAME_FIELD_RADIOTAP_MCS_INDEX = 'radiotap.mcs_index' |
| FRAME_FIELD_WLAN_FRAME_TYPE = 'wlan.fc_type_subtype' |
| FRAME_FIELD_WLAN_MGMT_SSID = 'wlan_mgt.ssid' |
| RADIOTAP_KNOWN_BAD_FCS_REJECTOR = ( |
| 'not radiotap.flags.badfcs or radiotap.flags.badfcs==0') |
| WLAN_BEACON_FRAME_TYPE = '0x08' |
| WLAN_BEACON_ACCEPTOR = 'wlan.fc.type_subtype==0x08' |
| WLAN_PROBE_REQ_FRAME_TYPE = '0x04' |
| WLAN_PROBE_REQ_ACCEPTOR = 'wlan.fc.type_subtype==0x04' |
| PYSHARK_BROADCAST_SSID = 'SSID: ' |
| BROADCAST_SSID = '' |
| |
| |
| class Frame(object): |
| """A frame from a packet capture.""" |
| TIME_FORMAT = "%H:%M:%S.%f" |
| |
| |
| def __init__(self, frametime, bit_rate, mcs_index, ssid): |
| self._datetime = frametime |
| self._bit_rate = bit_rate |
| self._mcs_index = mcs_index |
| self._ssid = ssid |
| |
| |
| @property |
| def time_datetime(self): |
| """The time of the frame, as a |datetime| object.""" |
| return self._datetime |
| |
| |
| @property |
| def bit_rate(self): |
| """The bitrate used to transmit the frame, as an int.""" |
| return self._bit_rate |
| |
| |
| @property |
| def mcs_index(self): |
| """ |
| The MCS index used to transmit the frame, as an int. |
| |
| The value may be None, if the frame was not transmitted |
| using 802.11n modes. |
| """ |
| return self._mcs_index |
| |
| |
| @property |
| def ssid(self): |
| """ |
| The SSID of the frame, as a string. |
| |
| The value may be None, if the frame does not have an SSID. |
| """ |
| return self._ssid |
| |
| |
| @property |
| def time_string(self): |
| """The time of the frame, in local time, as a string.""" |
| return self._datetime.strftime(self.TIME_FORMAT) |
| |
| |
| def _fetch_frame_field_value(frame, field): |
| """ |
| Retrieve the value of |field| within the |frame|. |
| |
| @param frame: Pyshark packet object corresponding to a captured frame. |
| @param field: Field for which the value needs to be extracted from |frame|. |
| |
| @return Value extracted from the frame if the field exists, else None. |
| |
| """ |
| layer_object = frame |
| for layer in field.split('.'): |
| try: |
| layer_object = getattr(layer_object, layer) |
| except AttributeError: |
| return None |
| return layer_object |
| |
| |
| def _open_capture(pcap_path, display_filter): |
| """ |
| Get pyshark packet object parsed contents of a pcap file. |
| |
| @param pcap_path: string path to pcap file. |
| @param display_filter: string filter to apply to captured frames. |
| |
| @return list of Pyshark packet objects. |
| |
| """ |
| import pyshark |
| capture = pyshark.FileCapture( |
| input_file=pcap_path, display_filter=display_filter) |
| capture.load_packets(timeout=PYSHARK_LOAD_TIMEOUT) |
| return capture |
| |
| |
| def get_frames(local_pcap_path, display_filter, bad_fcs): |
| """ |
| Get a parsed representation of the contents of a pcap file. |
| |
| @param local_pcap_path: string path to a local pcap file on the host. |
| @param diplay_filter: string filter to apply to captured frames. |
| @param bad_fcs: string 'include' or 'discard' |
| |
| @return list of Frame structs. |
| |
| """ |
| if bad_fcs == 'include': |
| display_filter = display_filter |
| elif bad_fcs == 'discard': |
| display_filter = '(%s) and (%s)' % (RADIOTAP_KNOWN_BAD_FCS_REJECTOR, |
| display_filter) |
| else: |
| raise error.TestError('Invalid value for bad_fcs arg: %s.' % bad_fcs) |
| |
| logging.debug('Capture: %s, Filter: %s', local_pcap_path, display_filter) |
| capture_frames = _open_capture(local_pcap_path, display_filter) |
| frames = [] |
| logging.info('Parsing frames') |
| |
| for frame in capture_frames: |
| rate = _fetch_frame_field_value(frame, FRAME_FIELD_RADIOTAP_DATARATE) |
| if rate: |
| rate = float(rate) |
| else: |
| logging.debug('Found bad capture frame: %s', frame) |
| continue |
| |
| frametime = frame.sniff_time |
| |
| mcs_index = _fetch_frame_field_value( |
| frame, FRAME_FIELD_RADIOTAP_MCS_INDEX) |
| if mcs_index: |
| mcs_index = int(mcs_index) |
| |
| # Get the SSID for any probe requests |
| frame_type = _fetch_frame_field_value( |
| frame, FRAME_FIELD_WLAN_FRAME_TYPE) |
| if (frame_type in [WLAN_BEACON_FRAME_TYPE, WLAN_PROBE_REQ_FRAME_TYPE]): |
| ssid = _fetch_frame_field_value(frame, FRAME_FIELD_WLAN_MGMT_SSID) |
| # Since the SSID name is a variable length field, there seems to be |
| # a bug in the pyshark parsing, it returns 'SSID: ' instead of '' |
| # for broadcast SSID's. |
| if ssid == PYSHARK_BROADCAST_SSID: |
| ssid = BROADCAST_SSID |
| else: |
| ssid = None |
| |
| frames.append(Frame(frametime, rate, mcs_index, ssid)) |
| |
| return frames |
| |
| |
| def get_probe_ssids(local_pcap_path, probe_sender=None): |
| """ |
| Get the SSIDs that were named in 802.11 probe requests frames. |
| |
| Parse a pcap, returning all the SSIDs named in 802.11 probe |
| request frames. If |probe_sender| is specified, only probes |
| from that MAC address will be considered. |
| |
| @param pcap_path: string path to a local pcap file on the host. |
| @param remote_host: Host object (if the file is remote). |
| @param probe_sender: MAC address of the device sending probes. |
| |
| @return: A frozenset of the SSIDs that were probed. |
| |
| """ |
| if probe_sender: |
| diplay_filter = '%s and wlan.addr==%s' % ( |
| WLAN_PROBE_REQ_ACCEPTOR, probe_sender) |
| else: |
| diplay_filter = WLAN_PROBE_REQ_ACCEPTOR |
| |
| frames = get_frames(local_pcap_path, diplay_filter, bad_fcs='discard') |
| |
| return frozenset( |
| [frame.ssid for frame in frames if frame.ssid is not None]) |