2 # -*- coding: utf-8 -*-
4 # config.py - support for WiFi Radar configuration
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) 2006, 2009 Ante Karamatic <ivoks@ubuntu.com>
15 # Copyright (C) 2009-2010,2014 Sean Robinson <robinson@tuxfamily.org>
16 # Copyright (C) 2010 Prokhor Shuchalov <p@shuchalov.ru>
18 # This program is free software; you can redistribute it and/or modify
19 # it under the terms of the GNU General Public License as published by
20 # the Free Software Foundation; version 2 of the License.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License in LICENSE.GPL for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to:
29 # Free Software Foundation, Inc.
30 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
34 from __future__
import unicode_literals
41 from shutil
import move
42 from subprocess
import Popen
, PIPE
, STDOUT
45 from wifiradar
.misc
import *
51 logger
= logging
.getLogger(__name__
)
54 def copy_configuration(original
, profiles
=False):
55 """ Return a :class:`ConfigManager` copy of :dat:`original`. If
56 :data:`profiles` is False (the default), the copy does not include
57 the known AP profiles.
59 config_copy
= ConfigManager()
60 config_copy
._defaults
= original
._defaults
.copy()
61 config_copy
._sections
= original
._sections
.copy()
62 # If not needed, remove the profiles from the new copy.
64 for section
in config_copy
.profiles():
65 config_copy
.remove_section(section
)
68 def make_section_name(essid
, bssid
):
69 """ Returns the combined :data:`essid` and :data:`bssid` to make a
70 config file section name. :data:`essid` and :data:`bssid` are
73 return essid
+ ':' + bssid
76 class ConfigManager(object, configparser
.SafeConfigParser
):
77 """ Manage configuration options, grouped into sections.
78 :class:`ConfigManager` is based on the standard library's
79 configparser API and is meant as a less capable replacement.
81 def __init__(self
, defaults
=None):
82 """ Create a new configuration manager.
84 configparser
.SafeConfigParser
.__init
__(self
, defaults
)
85 self
.auto_profile_order
= []
87 def get_network_device(self
):
88 """ Return the network device name.
90 If a device is specified in the configuration file,
91 :meth:`get_network_device` returns that value. If the
92 configuration is set to "auto-detect", this method returns the
93 first WiFi device as returned by iwconfig.
94 :meth:`get_network_device` raises
95 :exc:`wifiradar.misc.NoDeviceError` if no wireless device can
96 be found in auto-detect mode.
98 device
= self
.get_opt('GENERAL', 'interface')
99 if device
== 'auto_detect':
100 # auto detect network device
102 self
.get_opt('GENERAL', 'iwconfig_command'),
105 iwconfig_info
= Popen(iwconfig_command
, stdout
=PIPE
,
106 stderr
=STDOUT
).stdout
107 wireless_devices
= list()
108 for line
in iwconfig_info
:
110 name
= line
[0:line
.find(' ')]
111 wireless_devices
.append(name
)
112 # return the first device in the list
113 return wireless_devices
[0]
115 logger
.critical(_('problem auto-detecting wireless '
116 'device using iwconfig: {EXC}').format(EXC
=e
))
118 logger
.critical(_('No WiFi device found, '
119 'please set this in the preferences.'))
120 raise NoDeviceError('No WiFi device found.')
122 # interface has been manually specified in configuration
125 def set_section(self
, section
, dictionary
):
126 """ Set the options of :data:`section` to values from
129 :data:`section` will be created if it does not exist. The keys
130 of :data:`dictionary` are the options and its values are the
133 for option
, value
in dictionary
.items():
134 if isinstance(value
, BooleanType
):
135 self
.set_bool_opt(section
, option
, value
)
136 elif isinstance(value
, IntType
):
137 self
.set_int_opt(section
, option
, value
)
138 elif isinstance(value
, FloatType
):
139 self
.set_float_opt(section
, option
, value
)
141 self
.set_opt(section
, option
, value
)
143 def get_profile(self
, section
):
144 """ Return the profile values in :data:`section` as a dictionary.
146 :meth:`get_profile` raises :exc:`NoSectionError` if the profile
149 str_types
= ['bssid', 'channel', 'essid', 'protocol',
150 'con_prescript', 'con_postscript', 'dis_prescript',
151 'dis_postscript', 'key', 'mode', 'security',
152 'wpa_driver', 'ip', 'netmask', 'gateway', 'domain',
154 bool_types
= ['known', 'available', 'roaming', 'encrypted',
155 'use_wpa', 'use_dhcp']
156 int_types
= ['signal']
157 profile
= get_new_profile()
158 for option
in bool_types
:
160 profile
[option
] = self
.get_opt_as_bool(section
, option
)
161 except configparser
.NoOptionError
:
162 # A missing option means the default will be used.
164 for option
in int_types
:
166 profile
[option
] = self
.get_opt_as_int(section
, option
)
167 except configparser
.NoOptionError
:
168 # A missing option means the default will be used.
170 for option
in str_types
:
172 profile
[option
] = self
.get_opt(section
, option
)
173 except configparser
.NoOptionError
:
174 # A missing option means the default will be used.
178 def get_opt(self
, section
, option
):
179 """ Return the value of 'option' in 'section', as a string.
181 'section' and 'option' must be strings.
183 'get_opt' raises NoSectionError when 'section' is unknown and
184 NoOptionError when 'option' in unknown.
186 # False means to use interpolation when retrieving the value.
187 return self
.get(section
, option
, False)
189 def get_opt_as_bool(self
, section
, option
):
190 """ Return the value of :data:`option` in :data:`section`, as a
193 :meth:`get_opt_as_bool` calls :meth:`get_opt` before converting
194 the string to a boolean and raises :exc:`ValueError` if the
195 conversion is not possible.
197 value
= self
.get_opt(section
, option
)
200 elif value
== 'False':
202 raise ValueError('value was not True or False')
204 def get_opt_as_int(self
, section
, option
):
205 """ Return the value of :data:`option` in :data:`section`, as an
208 :meth:`get_opt_as_int` calls :meth:`get_opt` before converting
209 the string to an integer and raises :exc:`ValueError` if the
210 conversion is not possible.
212 value
= self
.get_opt(section
, option
)
215 def get_opt_as_float(self
, section
, option
):
216 """ Return the value of :data:`option` in :data:`section`, as a
219 :meth:`get_opt_as_float` calls :meth:`get_opt` before converting
220 the string to a float and raises :exc:`ValueError` if the
221 conversion is not possible.
223 value
= self
.get_opt(section
, option
)
226 def set_opt(self
, section
, option
, value
):
227 """ Set :data:`option` to :data:`value` in :data:`section`.
229 If :data:`section`, does not exist in the configuration, it is
230 created. If :data:`section` and :data:`option` are not strings,
231 each will be turned into one before being added. Raises
232 :exc:`TypeError` if :data:`value` is not a string.
234 section
, option
= str(section
), str(option
)
236 self
.set(section
, option
, value
)
237 except configparser
.NoSectionError
:
238 self
.add_section(section
)
239 self
.set_opt(section
, option
, value
)
241 def set_bool_opt(self
, section
, option
, value
):
242 """ Set :data:`option` to boolean :data:`value` in :data:`section`.
244 :meth:`set_bool_opt` calls :meth:`set_opt` after converting
245 :data:`value` to a string. Raises :exc:`ValueError` if
246 :data:`value` is not a boolean, where a boolean may be True,
247 'True', or a number greater than 0; or False, 'False', or 0.
249 if isinstance(value
, BooleanType
):
250 # use False = 0 and True = 1 to return index into tuple
251 value
= ('False', 'True')[value
]
252 elif isinstance(value
, IntType
):
254 raise ValueError(_('boolean value must be >= 0'))
255 # use False = 0 and True = 1 to return index into tuple
256 value
= ('False', 'True')[value
> 0]
257 elif isinstance(value
, StringTypes
):
258 # convert to title case (i.e. capital first letter, only)
259 value
= value
.title()
260 if value
not in ('False', 'True'):
261 raise ValueError(_('value must be "True" or "False"'))
263 raise ValueError(_('value cannot be converted to string'))
264 self
.set_opt(section
, option
, value
)
266 def set_int_opt(self
, section
, option
, value
):
267 """ Set :data:`option` to integer :data:`value` in :data:`section`.
269 :meth:`set_int_opt` calls :meth:`set_opt` after converting
270 :data:`value` to a string. Raises :exc:`TypeError` if
271 :data:`value` is not an integer.
273 if not isinstance(value
, IntType
):
274 raise TypeError(_('value is not an integer'))
275 self
.set_opt(section
, option
, str(value
))
277 def set_float_opt(self
, section
, option
, value
):
278 """ Set :data:`option` to float :data:`value` in :data:`section`.
280 :meth:`set_float_opt` calls :meth:`set_opt` after converting
281 :data:`value` to a string. Raises :exc:`TypeError` if
282 :data:`value` is not a float.
284 if not isinstance(value
, (FloatType
, IntType
)):
285 raise TypeError(_('value is not a float or integer'))
286 self
.set_opt(section
, option
, str(float(value
)))
289 """ Return a list of the section names which denote AP profiles.
291 :meth:`profiles` does not return non-AP sections.
294 for section
in self
.sections():
296 profile_list
.append(section
)
299 def update(self
, config_manager
):
300 """ Update internal configuration information using
301 :data:`config_manager`. This works by replacing the DEFAULT
302 and some non-profile sections in the configuration. All profiles,
303 and any non-profile sections not in :data:`config_manager`, are
304 left untouched during the update.
306 self
._defaults
= config_manager
._defaults
307 for section
in (set(config_manager
.sections()) -
308 set(config_manager
.profiles())):
309 self
.set_section(section
,
310 config_manager
._sections
[section
].copy())
313 class ConfigFileManager(configparser
.ConfigParser
):
314 """ Manage the configuration for the application, including reading
315 from and writing to a file.
317 def __init__(self
, filename
, defaults
=None):
318 """ Create a new configuration file at :data:`filename` with DEFAULT
319 options and values in the 'defaults' dictionary.
321 super(ConfigFileManager
, self
).__init
__(defaults
)
322 self
.filename
= filename
325 """ Read configuration file from disk into instance variables.
327 with codecs
.open(self
.filename
, 'r', encoding
='utf8') as f
:
328 self
.read_file(f
, self
.filename
)
329 # convert the auto_profile_order to a list for ordering
330 self
.auto_profile_order
= eval(self
.get_opt('GENERAL', 'auto_profile_order'))
331 for ap
in self
.profiles():
332 self
.set_bool_opt(ap
, 'known', True)
333 if ap
in self
.auto_profile_order
: continue
334 self
.auto_profile_order
.append(ap
)
335 # Remove any auto_profile_order AP without a matching section.
336 auto_profile_order_copy
= self
.auto_profile_order
[:]
337 for ap
in auto_profile_order_copy
:
338 if ap
not in self
.profiles():
339 self
.auto_profile_order
.remove(ap
)
342 """ Write configuration file to disk from instance variables.
344 Copied from configparser and modified to write options in
347 self
.set_opt('GENERAL', 'auto_profile_order',
348 str(self
.auto_profile_order
))
349 self
.set_opt('GENERAL', 'version', WIFI_RADAR_VERSION
)
350 # Safely create a temporary file, ...
351 (fd
, tempfilename
) = tempfile
.mkstemp(prefix
='wifi-radar.conf.')
352 # ....close the file descriptor to the temporary file, ...
354 # ....and re-open the temporary file with a codec filter.
355 with codecs
.open(tempfilename
, 'w', encoding
='utf8') as fp
:
356 # Write a byte-encoding note on the first line.
357 fp
.write('# -*- coding: utf-8 -*-\n')
358 # write DEFAULT section
360 fp
.write('[DEFAULT]\n')
361 for key
in sorted(self
._defaults
.keys()):
362 fp
.write('{KEY} = {VALUE}\n'.format(KEY
=key
,
363 VALUE
=str(self
._defaults
[key
]).replace('\n','\n\t')))
365 # write other non-profile sections next
366 for section
in self
._sections
:
367 if section
not in self
.profiles():
368 fp
.write('[{SECT}]\n'.format(SECT
=section
))
369 for key
in sorted(self
._sections
[section
].keys()):
370 if key
!= '__name__':
371 fp
.write('{KEY} = {VALUE}\n'.format(KEY
=key
,
372 VALUE
=str(self
._sections
[section
][key
]
373 ).replace('\n', '\n\t')))
375 # write profile sections
376 for section
in self
._sections
:
377 if section
in self
.profiles():
378 fp
.write('[{SECT}]\n'.format(SECT
=section
))
379 for key
in sorted(self
._sections
[section
].keys()):
380 if key
!= '__name__':
381 fp
.write('{KEY} = {VALUE}\n'.format(KEY
=key
,
382 VALUE
=str(self
._sections
[section
][key
]
383 ).replace('\n', '\n\t')))
385 move(tempfilename
, self
.filename
)
388 # Make so we can be imported
389 if __name__
== '__main__':