1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import os
6import subprocess
7
8from autotest_lib.client.bin import utils
9from autotest_lib.client.common_lib.cros import site_eap_certs
10
11class HostapdServer(object):
12    """Hostapd server instance wrapped in a context manager.
13
14    Simple interface to starting and controlling a hsotapd instance.
15    This can be combined with a virtual-ethernet setup to test 802.1x
16    on a wired interface.
17
18    Example usage:
19        with hostapd_server.HostapdServer(interface='veth_master') as hostapd:
20            hostapd.send_eap_packets()
21
22    """
23    CONFIG_TEMPLATE = """
24interface=%(interface)s
25driver=%(driver)s
26logger_syslog=-1
27logger_syslog_level=2
28logger_stdout=-1
29logger_stdout_level=2
30dump_file=%(config_directory)s/hostapd.dump
31ctrl_interface=%(config_directory)s/%(control_directory)s
32ieee8021x=1
33eapol_key_index_workaround=0
34eap_server=1
35eap_user_file=%(config_directory)s/%(user_file)s
36ca_cert=%(config_directory)s/%(ca_cert)s
37server_cert=%(config_directory)s/%(server_cert)s
38private_key=%(config_directory)s/%(server_key)s
39use_pae_group_addr=1
40eap_reauth_period=10
41"""
42    CA_CERTIFICATE_FILE = 'ca.crt'
43    CONFIG_FILE = 'hostapd.conf'
44    CONTROL_DIRECTORY = 'hostapd.ctl'
45    EAP_PASSWORD = 'password'
46    EAP_PHASE2 = 'MSCHAPV2'
47    EAP_TYPE = 'PEAP'
48    EAP_USERNAME = 'test'
49    HOSTAPD_EXECUTABLE = 'hostapd'
50    HOSTAPD_CLIENT_EXECUTABLE = 'hostapd_cli'
51    SERVER_CERTIFICATE_FILE = 'server.crt'
52    SERVER_PRIVATE_KEY_FILE = 'server.key'
53    USER_AUTHENTICATION_TEMPLATE = """* %(type)s
54"%(username)s"\t%(phase2)s\t"%(password)s"\t[2]
55"""
56    USER_FILE = 'hostapd.eap_user'
57    # This is the default group MAC address to which EAP challenges
58    # are sent, absent any prior knowledge of a specific client on
59    # the link.
60    PAE_NEAREST_ADDRESS = '01:80:c2:00:00:03'
61
62    def __init__(self,
63                 interface=None,
64                 driver='wired',
65                 config_directory='/tmp/hostapd-test'):
66        super(HostapdServer, self).__init__()
67        self._interface = interface
68        self._config_directory = config_directory
69        self._control_directory = '%s/%s' % (self._config_directory,
70                                             self.CONTROL_DIRECTORY)
71        self._driver = driver
72        self._process = None
73
74
75    def __enter__(self):
76        self.start()
77        return self
78
79
80    def __exit__(self, exception, value, traceback):
81        self.stop()
82
83
84    def write_config(self):
85        """Write out a hostapd configuration file-set based on the caller
86        supplied parameters.
87
88        @return the file name of the top-level configuration file written.
89
90        """
91        if not os.path.exists(self._config_directory):
92            os.mkdir(self._config_directory)
93        config_params = {
94            'ca_cert': self.CA_CERTIFICATE_FILE,
95            'config_directory' : self._config_directory,
96            'control_directory': self.CONTROL_DIRECTORY,
97            'driver': self._driver,
98            'interface': self._interface,
99            'server_cert': self.SERVER_CERTIFICATE_FILE,
100            'server_key': self.SERVER_PRIVATE_KEY_FILE,
101            'user_file': self.USER_FILE
102        }
103        authentication_params = {
104            'password': self.EAP_PASSWORD,
105            'phase2': self.EAP_PHASE2,
106            'username': self.EAP_USERNAME,
107            'type': self.EAP_TYPE
108        }
109        for filename, contents in (
110                ( self.CA_CERTIFICATE_FILE, site_eap_certs.ca_cert_1 ),
111                ( self.CONFIG_FILE, self.CONFIG_TEMPLATE % config_params),
112                ( self.SERVER_CERTIFICATE_FILE, site_eap_certs.server_cert_1 ),
113                ( self.SERVER_PRIVATE_KEY_FILE,
114                  site_eap_certs.server_private_key_1 ),
115                ( self.USER_FILE,
116                  self.USER_AUTHENTICATION_TEMPLATE % authentication_params )):
117            config_file = '%s/%s' % (self._config_directory, filename)
118            with open(config_file, 'w') as f:
119                f.write(contents)
120        return '%s/%s' % (self._config_directory, self.CONFIG_FILE)
121
122
123    def start(self):
124        """Start the hostap server."""
125        config_file = self.write_config()
126        self._process = subprocess.Popen(
127                 [self.HOSTAPD_EXECUTABLE, '-dd', config_file])
128
129
130    def stop(self):
131        """Stop the hostapd server."""
132        if self._process:
133            self._process.terminate()
134            self._process.wait()
135            self._process = None
136
137
138    def running(self):
139        """Tests whether the hostapd process is still running.
140
141        @return True if the hostapd process is still running, False otherwise.
142
143        """
144        if not self._process:
145            return False
146
147        if self._process.poll() != None:
148            # We have essentially reaped the proces, and it is no more.
149            self._process = None
150            return False
151
152        return True
153
154
155    def send_eap_packets(self):
156        """Start sending EAP packets to the nearest neighbor."""
157        self.send_command('new_sta %s' % self.PAE_NEAREST_ADDRESS)
158
159
160    def get_client_mib(self, client_mac_address):
161        """Get a dict representing the MIB properties for |client_mac_address|.
162
163        @param client_mac_address string MAC address of the client.
164        @return dict containing mib properties.
165
166        """
167        # Expected output of "hostapd cli <client_mac_address>":
168        #
169        #     Selected interface 'veth_master'
170        #     b6:f1:39:1d:ad:10
171        #     dot1xPaePortNumber=0
172        #     dot1xPaePortProtocolVersion=2
173        #     [...]
174        result = self.send_command('sta %s' % client_mac_address)
175        client_mib = {}
176        found_client = False
177        for line in result.splitlines():
178            if found_client:
179                parts = line.split('=', 1)
180                if len(parts) == 2:
181                    client_mib[parts[0]] = parts[1]
182            elif line == client_mac_address:
183                found_client = True
184        return client_mib
185
186
187    def send_command(self, command):
188        """Send a command to the hostapd instance.
189
190        @param command string containing the command to run on hostapd.
191        @return string output of the command.
192
193        """
194        return utils.system_output('%s -p %s %s' %
195                                   (self.HOSTAPD_CLIENT_EXECUTABLE,
196                                    self._control_directory, command))
197
198
199    def client_has_authenticated(self, client_mac_address):
200        """Return whether |client_mac_address| has successfully authenticated.
201
202        @param client_mac_address string MAC address of the client.
203        @return True if client is authenticated.
204
205        """
206        mib = self.get_client_mib(client_mac_address)
207        return mib.get('dot1xAuthAuthSuccessesWhileAuthenticating', '') == '1'
208
209
210    def client_has_logged_off(self, client_mac_address):
211        """Return whether |client_mac_address| has logged-off.
212
213        @param client_mac_address string MAC address of the client.
214        @return True if client has logged off.
215
216        """
217        mib = self.get_client_mib(client_mac_address)
218        return mib.get('dot1xAuthAuthEapLogoffWhileAuthenticated', '') == '1'
219