2 # -*- coding: utf-8 -*-
4 # connections.py - collection of classes for supporting WiFi connections
6 # Part of WiFi Radar: A utility for managing WiFi profiles on GNU/Linux.
8 # Copyright (C) 2004-2005 Ahmad Baitalmal <ahmad@baitalmal.com>
9 # Copyright (C) 2005 Nicolas Brouard <nicolas.brouard@mandrake.org>
10 # Copyright (C) 2005-2009 Brian Elliott Finley <brian@thefinleys.com>
11 # Copyright (C) 2006 David Decotigny <com.d2@free.fr>
12 # Copyright (C) 2006 Simon Gerber <gesimu@gmail.com>
13 # Copyright (C) 2006-2007 Joey Hurst <jhurst@lucubrate.org>
14 # Copyright (C) 2012 Anari Jalakas <anari.jalakas@gmail.com>
15 # Copyright (C) 2006, 2009 Ante Karamatic <ivoks@ubuntu.com>
16 # Copyright (C) 2009-2010,2014 Sean Robinson <seankrobinson@gmail.com>
17 # Copyright (C) 2010 Prokhor Shuchalov <p@shuchalov.ru>
19 # This program is free software; you can redistribute it and/or modify
20 # it under the terms of the GNU General Public License as published by
21 # the Free Software Foundation; version 2 of the License.
23 # This program is distributed in the hope that it will be useful,
24 # but WITHOUT ANY WARRANTY; without even the implied warranty of
25 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 # GNU General Public License in LICENSE.GPL for more details.
28 # You should have received a copy of the GNU General Public License
29 # along with this program; if not, write to the Free Software
30 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
44 from signal
import SIGTERM
45 from subprocess
import CalledProcessError
, Popen
, PIPE
, STDOUT
46 from time
import sleep
48 from wifiradar
.config
import make_section_name
, ConfigManager
49 import wifiradar
.misc
as misc
52 logger
= logging
.getLogger(__name__
)
55 def get_enc_mode(use_wpa
, key
):
56 """ Return the WiFi encryption mode based on the combination of
57 'use_wpa' and 'key'. Possible return values are 'wpa', 'wep',
68 class ConnectionManager(object):
69 """ Manage a connection; including reporting connection state,
70 connecting/disconnecting from an AP, and returning current IP, ESSID, and BSSID.
72 def __init__(self
, msg_queue
):
73 """ Create a new connection manager which communicates with a
74 controlling process or thread through 'msg_queue' (a Queue
77 self
.msg_queue
= msg_queue
79 def set_config(self
, config_manager
):
80 """ Set the configuration manager to 'config'. This method
81 must be called before any other public method or a caller
82 will likely receive an AttributeError about a missing
85 Raises TypeError if 'config_manager' is not a ConfigManager
88 if not isinstance(config_manager
, ConfigManager
):
89 raise TypeError('config must be a ConfigManager object')
90 self
.config
= config_manager
92 def _run_script(self
, script_name
, profile
, device
):
93 """ Run the script (e.g. connection prescript) in 'profile' which
94 is named in 'script_name'.
96 if profile
[script_name
]:
97 logger
.info('running {}'.format(script_name
))
98 profile_name
= make_section_name(profile
['essid'], profile
['bssid'])
99 enc_mode
= get_enc_mode(profile
['use_wpa'], profile
['key'])
101 "WIFIRADAR_IP": self
.get_current_ip(),
102 "WIFIRADAR_ESSID": self
.get_current_essid(),
103 "WIFIRADAR_BSSID": self
.get_current_bssid(),
104 "WIFIRADAR_PROFILE": profile_name
,
105 "WIFIRADAR_ENCMODE": enc_mode
,
106 "WIFIRADAR_SECMODE": profile
['security'],
107 "WIFIRADAR_IF": device
}
109 misc
.shellcmd(profile
[script_name
].split(' '), custom_env
)
110 except CalledProcessError
as e
:
111 logger
.error('script "{}" failed: {}'.format(script_name
, e
))
112 self
.msg_queue
.put('script "{}" failed: {}'.format(
115 def _prepare_nic(self
, profile
, device
):
116 """ Configure the NIC for upcoming connection.
118 # Start building iwconfig command line.
120 self
.config
.get_opt('DEFAULT', 'iwconfig_command'),
122 'essid', "'{}'".format(profile
['essid']),
126 iwconfig_command
.append('key')
127 if (not profile
['key']) or (profile
['key'] == 's:'):
128 iwconfig_command
.append('off')
130 # Setting this stops association from working, so remove it for now
131 #if profile['security'] != '':
132 #iwconfig_command.append(profile['security'])
133 iwconfig_command
.append("'{}'".format(profile
['key']))
134 #iwconfig_commands.append( "key %s %s" % ( profile['security'], profile['key'] ) )
137 profile
['mode'] = profile
['mode'].lower()
138 if profile
['mode'] == 'master' or profile
['mode'] == 'auto':
139 profile
['mode'] = 'managed'
140 iwconfig_command
.extend(['mode', profile
['mode']])
143 if 'channel' in profile
:
144 iwconfig_command
.extend(['channel', profile
['channel']])
146 # Set AP address (do this last since iwconfig seems to want it only there).
147 iwconfig_command
.extend(['ap', profile
['bssid']])
149 # Some cards require a commit
150 if self
.config
.get_opt_as_bool('DEFAULT', 'commit_required'):
151 iwconfig_command
.append('commit')
153 logger
.debug('iwconfig_command: {}'.format(iwconfig_command
))
156 misc
.shellcmd(iwconfig_command
)
157 except CalledProcessError
as e
:
158 logger
.error('Failed to prepare NIC: {}'.format(e
))
159 self
.msg_queue
.put('Failed to prepare NIC: {}'.format(e
))
160 raise misc
.DeviceError('Could not configure wireless options.')
162 def _stop_dhcp(self
, device
):
163 """ Stop any DHCP client daemons running with our 'device'.
165 logger
.info('Stopping any DHCP clients on "{}"'.format(device
))
166 if os
.access(self
.config
.get_opt('DHCP', 'pidfile'), os
.R_OK
):
167 if self
.config
.get_opt('DHCP', 'kill_args'):
168 dhcp_command
= [self
.config
.get_opt('DHCP', 'command')]
169 dhcp_command
.extend(self
.config
.get_opt('DHCP', 'kill_args').split(' '))
170 dhcp_command
.append(device
)
171 logger
.info("DHCP command: %s" % (dhcp_command
, ))
173 # call DHCP client command and wait for return
174 logger
.info("Stopping DHCP with kill_args")
176 misc
.shellcmd(dhcp_command
)
177 except CalledProcessError
as e
:
178 logger
.error('Attempt to stop DHCP failed: {}'.format(e
))
180 logger
.info("Stopping DHCP manually...")
181 os
.kill(int(open(self
.config
.get_opt('DHCP', 'pidfile'), mode
='r').readline()), SIGTERM
)
183 def _start_dhcp(self
, device
):
184 """ Start a DHCP client daemon on 'device'.
186 logger
.debug('Starting DHCP command on "{}"'.format(device
))
187 self
.msg_queue
.put("Acquiring IP Address (DHCP)")
188 dhcp_command
= [self
.config
.get_opt('DHCP', 'command')]
189 dhcp_command
.extend(self
.config
.get_opt('DHCP', 'args').split(' '))
190 dhcp_command
.append(device
)
191 logger
.info("dhcp_command: %s" % (dhcp_command
, ))
193 dhcp_proc
= Popen(dhcp_command
, stdout
=None, stderr
=None)
196 logger
.critical('DHCP client not found, ' + \
197 'please set this in the preferences.')
199 # The DHCP client daemon should timeout on its own, hence
200 # the +3 seconds on timeout so we don't cut the daemon off
201 # while it is finishing up.
202 timeout
= self
.config
.get_opt_as_int('DHCP', 'timeout') + 3
204 dhcp_status
= dhcp_proc
.poll()
205 while dhcp_status
is None:
207 dhcp_proc
.terminate()
209 timeout
= timeout
- tick
211 dhcp_status
= dhcp_proc
.poll()
212 if self
.get_current_ip() is not None:
213 self
.msg_queue
.put("Got IP address. Done.")
215 self
.msg_queue
.put("Could not get IP address!")
218 """ Stop all WPA supplicants.
220 logger
.info("Kill off any existing WPA supplicants running...")
221 if os
.access(self
.config
.get_opt('WPA', 'pidfile'), os
.R_OK
):
222 logger
.info("Killing existing WPA supplicant...")
224 if self
.config
.get_opt('WPA', 'kill_command'):
225 wpa_command
= [self
.config
.get_opt('WPA', 'kill_command').split(' ')]
227 misc
.shellcmd(wpa_command
)
228 except CalledProcessError
as e
:
229 logger
.error('Attempt to stop WPA supplicant ' + \
230 'failed: {}'.format(e
))
232 os
.kill(int(open(self
.config
.get_opt('WPA', 'pidfile'), mode
='r').readline()), SIGTERM
)
234 logger
.info("Failed to kill WPA supplicant")
236 def _start_wpa(self
):
237 """ Start WPA supplicant and let it associate with the AP.
239 self
.msg_queue
.put("WPA supplicant starting")
241 wpa_command
= [self
.config
.get_opt('WPA', 'command')]
242 wpa_command
.extend(self
.config
.get_opt('WPA', 'args').split(' '))
244 misc
.shellcmd(wpa_command
)
245 except CalledProcessError
as e
:
246 logger
.error('WPA supplicant failed to start: {}'.format(e
))
248 def _start_manual_network(self
, profile
, device
):
249 """ Manually configure network settings after association.
251 # Bring down the interface before trying to make changes.
253 self
.config
.get_opt('DEFAULT', 'ifconfig_command'),
256 misc
.shellcmd(ifconfig_command
)
257 except CalledProcessError
as e
:
258 logger
.error('Device "{}" failed to go down: {}'.format(device
, e
))
259 # Bring the interface up with our manual IP.
261 self
.config
.get_opt('DEFAULT', 'ifconfig_command'),
262 device
, profile
['ip'],
263 'netmask', profile
['netmask']]
265 misc
.shellcmd(ifconfig_command
)
266 except CalledProcessError
as e
:
267 logger
.error('Device "{}" failed to configure: {}'.format(device
, e
))
268 # Configure routing information.
270 self
.config
.get_opt('DEFAULT', 'route_command'),
271 'add', 'default', 'gw', profile
['gateway']]
273 misc
.shellcmd(route_command
)
274 except CalledProcessError
as e
:
275 logger
.error('Failed to configure routing information: {}'.format(e
))
276 # Build the /etc/resolv.conf file, if needed.
278 if profile
['domain']:
279 resolv_contents
+= "domain {}\n".format(profile
['domain'])
281 resolv_contents
+= "nameserver {}\n".format(profile
['dns1'])
283 resolv_contents
+= "nameserver {}\n".format(profile
['dns2'])
285 with
open('/etc/resolv.conf', 'w') as resolv_file
:
286 resolv_file
.write(resolv_contents
)
288 def connect(self
, profile
):
289 """ Connect to the access point specified by 'profile'.
291 if profile
['bssid'] == '':
292 raise TypeError("Empty AP address")
293 msg
= "Connecting to the %s (%s) network" % ( profile
['essid'], profile
['bssid'] )
295 # Make a temporary copy of the DEFAULT.interface option.
296 default_interface
= self
.config
.get_opt('DEFAULT', 'interface')
297 device
= self
.config
.get_network_device()
298 # Temporarily set the configured interface to a real one.
299 self
.config
.set_opt('DEFAULT', 'interface', device
)
301 # Let's run the connection prescript
302 self
._run
_script
('con_prescript', profile
, device
)
303 # Some cards need to have the interface up
304 if self
.config
.get_opt_as_bool('DEFAULT', 'ifup_required'):
307 self
._prepare
_nic
(profile
, device
)
309 # Now normal network stuff
311 self
._stop
_dhcp
(device
)
314 logger
.debug("Disable scan while connection attempt in progress...")
316 self
.msg_queue
.put("pause")
320 if profile
['use_wpa'] :
323 if profile
['use_dhcp'] :
324 self
._start
_dhcp
(device
)
326 self
._start
_manual
_network
(profile
, device
)
330 self
.msg_queue
.put("scan")
333 # Let's run the connection postscript
334 self
._run
_script
('con_postscript', profile
, device
)
335 # Set the configured interface back to original value.
336 self
.config
.set_opt('DEFAULT', 'interface', default_interface
)
338 def disconnect(self
, profile
):
339 """ Disconnect from the AP with which a connection has been
340 established/attempted.
342 msg
= "Disconnecting"
344 # Make a temporary copy of the DEFAULT.interface option.
345 default_interface
= self
.config
.get_opt('DEFAULT', 'interface')
346 device
= self
.config
.get_network_device()
347 # Temporarily set the configured interface to a real one.
348 self
.config
.set_opt('DEFAULT', 'interface', device
)
349 # Pause scanning while manipulating card
351 self
.msg_queue
.put("pause")
352 self
.msg_queue
.join()
355 profile
= self
.config
.get_profile(make_section_name(self
.get_current_essid(), self
.get_current_bssid()))
357 profile
= self
.config
.get_profile(make_section_name(self
.get_current_essid(), ''))
360 # Let's run the disconnection prescript
361 self
._run
_script
('dis_prescript', profile
, device
)
363 self
._stop
_dhcp
(device
)
366 logger
.info("Let's clear out the wireless stuff")
367 misc
.shellcmd([self
.config
.get_opt('DEFAULT', 'iwconfig_command'), device
, 'essid', 'any', 'key', 'off', 'mode', 'managed', 'channel', 'auto', 'ap', 'off'])
368 logger
.info("Now take the interface down")
369 logger
.info("Since it may be brought back up by the next scan, lets unset its IP")
370 misc
.shellcmd([self
.config
.get_opt('DEFAULT', 'ifconfig_command'), device
, '0.0.0.0'])
371 # taking down the interface too quickly can crash my system, so pause a moment
373 self
.if_change('down')
374 # Let's run the disconnection postscript
375 self
._run
_script
('dis_postscript', profile
, device
)
376 logger
.info("Disconnect complete.")
377 # Begin scanning again
379 self
.msg_queue
.put("scan")
382 # Set the configured interface back to original value.
383 self
.config
.set_opt('DEFAULT', 'interface', default_interface
)
385 def if_change(self
, state
):
386 """ Change the interface to 'state', i.e. 'up' or 'down'.
388 'if_change' raises ValueError if 'state' is not recognized,
389 raises OSError if there is a problem running ifconfig, and
390 raises DeviceError if ifconfig reports the change failed.
392 state
= state
.lower()
393 if ((state
== 'up') or (state
== 'down')):
394 device
= self
.config
.get_network_device()
397 self
.config
.get_opt('DEFAULT', 'ifconfig_command'),
400 logger
.info('changing interface ' +
401 '{} state to {}'.format(device
, state
))
402 ifconfig_info
= Popen(ifconfig_command
, stdout
=PIPE
,
403 stderr
=STDOUT
).stdout
406 logger
.critical("ifconfig command not found, " +
407 "please set this in the preferences.")
410 for line
in ifconfig_info
:
412 raise misc
.DeviceError('Could not change ' +
413 'device state: {}'.format(line
))
415 raise ValueError('unrecognized state for device: {}'.format(state
))
417 def get_current_ip(self
):
418 """ Return the current IP address as a string or None.
420 device
= self
.config
.get_network_device()
423 self
.config
.get_opt('DEFAULT', 'ifconfig_command'),
426 ifconfig_info
= Popen(ifconfig_command
, stdout
=PIPE
).stdout
427 # Be careful to the language (inet adr: in French for example)
431 # I'm using wifi-radar on a system with German translations (de_CH-UTF-8).
432 # There the string in ifconfig is inet Adresse for the IP which isn't
433 # found by the current get_current_ip function in wifi-radar. I changed
434 # the according line (#289; gentoo, v1.9.6-r1) to
435 # >ip_re = re.compile(r'inet [Aa]d?dr[^.]*:([^.]*\.[^.]*\.[^.]*\.[0-9]*)')
436 # which works on my system (LC_ALL=de_CH.UTF-8) and still works with LC_ALL=C.
438 # I'd be happy if you could incorporate this small change because as now
439 # I've got to change the file every time it is updated.
444 ip_re
= re
.compile(r
'inet [Aa]d?dr[^.]*:([^.]*\.[^.]*\.[^.]*\.[0-9]*)')
445 line
= ifconfig_info
.read()
446 if ip_re
.search(line
):
447 return ip_re
.search(line
).group(1)
450 logger
.critical("ifconfig command not found, " +
451 "please set this in the preferences.")
454 def get_current_essid(self
):
455 """ Return the current ESSID as a string or None.
457 device
= self
.config
.get_network_device()
460 self
.config
.get_opt('DEFAULT', 'iwconfig_command'),
463 iwconfig_info
= Popen(iwconfig_command
, stdout
=PIPE
, stderr
=STDOUT
).stdout
464 essid_re
= re
.compile(r
'ESSID\s*(:|=)\s*"([^"]+)"',
466 line
= iwconfig_info
.read()
467 if essid_re
.search(line
):
468 return essid_re
.search(line
).group(2)
471 logger
.critical("iwconfig command not found, " +
472 "please set this in the preferences.")
475 def get_current_bssid(self
):
476 """ Return the current BSSID as a string or None.
478 device
= self
.config
.get_network_device()
481 self
.config
.get_opt('DEFAULT', 'iwconfig_command'),
484 iwconfig_info
= Popen(iwconfig_command
, stdout
=PIPE
, stderr
=STDOUT
).stdout
485 bssid_re
= re
.compile(r
'Access Point\s*(:|=)\s*([a-fA-F0-9:]{17})',
487 line
= iwconfig_info
.read()
488 if bssid_re
.search(line
):
489 return bssid_re
.search(line
).group(2)
492 logger
.critical("iwconfig command not found, " +
493 "please set this in the preferences.")
497 # Make so we can be imported
498 if __name__
== "__main__":