| 1 | |
|---|
| 2 | |
|---|
| 3 | |
|---|
| 4 | |
|---|
| 5 | |
|---|
| 6 | |
|---|
| 7 | |
|---|
| 8 | |
|---|
| 9 | |
|---|
| 10 | |
|---|
| 11 | |
|---|
| 12 | |
|---|
| 13 | |
|---|
| 14 | |
|---|
| 15 | |
|---|
| 16 | |
|---|
| 17 | |
|---|
| 18 | |
|---|
| 19 | |
|---|
| 20 | |
|---|
| 21 | """Manage the configuration of a CryptoBox |
|---|
| 22 | """ |
|---|
| 23 | |
|---|
| 24 | __revision__ = "$Id$" |
|---|
| 25 | |
|---|
| 26 | from cryptobox.core.exceptions import * |
|---|
| 27 | import logging |
|---|
| 28 | import subprocess |
|---|
| 29 | import os |
|---|
| 30 | import configobj, validate |
|---|
| 31 | import syslog |
|---|
| 32 | |
|---|
| 33 | |
|---|
| 34 | CONF_LOCATIONS = [ |
|---|
| 35 | "./cryptobox.conf", |
|---|
| 36 | "~/.cryptobox.conf", |
|---|
| 37 | "/etc/cryptobox-server/cryptobox.conf"] |
|---|
| 38 | |
|---|
| 39 | VOLUMESDB_FILE = "cryptobox_volumes.db" |
|---|
| 40 | PLUGINCONF_FILE = "cryptobox_plugins.conf" |
|---|
| 41 | USERDB_FILE = "cryptobox_users.db" |
|---|
| 42 | |
|---|
| 43 | |
|---|
| 44 | CURRENT_SETTING = [] |
|---|
| 45 | |
|---|
| 46 | |
|---|
| 47 | def get_current_settings(): |
|---|
| 48 | """return the most recently created setting object |
|---|
| 49 | """ |
|---|
| 50 | if not CURRENT_SETTING: |
|---|
| 51 | return None |
|---|
| 52 | else: |
|---|
| 53 | return CURRENT_SETTING[0] |
|---|
| 54 | |
|---|
| 55 | |
|---|
| 56 | |
|---|
| 57 | class CryptoBoxSettings: |
|---|
| 58 | """Manage the various configuration files of the CryptoBox |
|---|
| 59 | """ |
|---|
| 60 | |
|---|
| 61 | def __init__(self, config_file=None): |
|---|
| 62 | self.__is_initialized = False |
|---|
| 63 | self.log = logging.getLogger("CryptoNAS") |
|---|
| 64 | config_file = self.__get_config_filename(config_file) |
|---|
| 65 | self.log.info("loading config file: %s" % config_file) |
|---|
| 66 | self.prefs = self.__get_preferences(config_file) |
|---|
| 67 | if not "PluginSettings" in self.prefs: |
|---|
| 68 | self.prefs["PluginSettings"] = {} |
|---|
| 69 | self.__validate_config() |
|---|
| 70 | self.__configure_log_handler() |
|---|
| 71 | self.__check_unknown_preferences() |
|---|
| 72 | self.prepare_partition() |
|---|
| 73 | self.volumes_db = self.__get_volumes_database() |
|---|
| 74 | self.plugin_conf = self.__get_plugin_config() |
|---|
| 75 | self.user_db = self.__get_user_db() |
|---|
| 76 | self.misc_files = [] |
|---|
| 77 | self.reload_misc_files() |
|---|
| 78 | self.__is_initialized = True |
|---|
| 79 | CURRENT_SETTING.insert(0, self) |
|---|
| 80 | |
|---|
| 81 | |
|---|
| 82 | def reload_misc_files(self): |
|---|
| 83 | """Call this method after creating or removing a 'misc' configuration file |
|---|
| 84 | """ |
|---|
| 85 | self.misc_files = self.__get_misc_files() |
|---|
| 86 | |
|---|
| 87 | |
|---|
| 88 | def write(self): |
|---|
| 89 | """ |
|---|
| 90 | write all local setting files including the content of the "misc" subdirectory |
|---|
| 91 | """ |
|---|
| 92 | status = True |
|---|
| 93 | try: |
|---|
| 94 | self.volumes_db.write() |
|---|
| 95 | except IOError: |
|---|
| 96 | self.log.warn("Could not save the volume database") |
|---|
| 97 | status = False |
|---|
| 98 | try: |
|---|
| 99 | self.plugin_conf.write() |
|---|
| 100 | except IOError: |
|---|
| 101 | self.log.warn("Could not save the plugin configuration") |
|---|
| 102 | status = False |
|---|
| 103 | try: |
|---|
| 104 | self.user_db.write() |
|---|
| 105 | except IOError: |
|---|
| 106 | self.log.warn("Could not save the user database") |
|---|
| 107 | status = False |
|---|
| 108 | for misc_file in self.misc_files: |
|---|
| 109 | if not misc_file.save(): |
|---|
| 110 | self.log.warn("Could not save a misc setting file (%s)" % misc_file.filename) |
|---|
| 111 | status = False |
|---|
| 112 | return status |
|---|
| 113 | |
|---|
| 114 | |
|---|
| 115 | def get_misc_config_filename(self, name): |
|---|
| 116 | """Return an absolute filename for a given filename 'name' |
|---|
| 117 | |
|---|
| 118 | 'name' should not contain slashes (no directory part!) |
|---|
| 119 | """ |
|---|
| 120 | return os.path.join(self.prefs["Locations"]["SettingsDir"], "misc", name) |
|---|
| 121 | |
|---|
| 122 | |
|---|
| 123 | def create_misc_config_file(self, name, content): |
|---|
| 124 | """Create a new configuration file in the 'settings' directory |
|---|
| 125 | |
|---|
| 126 | "name" should be the basename (without a directory) |
|---|
| 127 | "content" will be directly written to the file |
|---|
| 128 | this method may throw an IOException |
|---|
| 129 | """ |
|---|
| 130 | misc_conf_file = self.get_misc_config_filename(name) |
|---|
| 131 | misc_conf_dir = os.path.dirname(misc_conf_file) |
|---|
| 132 | if not os.path.isdir(misc_conf_dir): |
|---|
| 133 | try: |
|---|
| 134 | os.mkdir(misc_conf_dir) |
|---|
| 135 | except OSError, err_msg: |
|---|
| 136 | |
|---|
| 137 | raise IOError, err_msg |
|---|
| 138 | cfile = open(misc_conf_file, "w") |
|---|
| 139 | try: |
|---|
| 140 | cfile.write(content) |
|---|
| 141 | except IOError: |
|---|
| 142 | cfile.close() |
|---|
| 143 | raise |
|---|
| 144 | cfile.close() |
|---|
| 145 | |
|---|
| 146 | self.reload_misc_files() |
|---|
| 147 | |
|---|
| 148 | |
|---|
| 149 | def requires_partition(self): |
|---|
| 150 | return bool(self.prefs["Main"]["UseConfigPartition"]) |
|---|
| 151 | |
|---|
| 152 | |
|---|
| 153 | def get_active_partition(self): |
|---|
| 154 | """Return the currently active cnfiguration partition. |
|---|
| 155 | """ |
|---|
| 156 | settings_dir = self.prefs["Locations"]["SettingsDir"] |
|---|
| 157 | if not os.path.ismount(settings_dir): |
|---|
| 158 | return None |
|---|
| 159 | for line in file("/proc/mounts"): |
|---|
| 160 | fields = line.split(" ") |
|---|
| 161 | mount_dir = fields[1] |
|---|
| 162 | fs_type = fields[2] |
|---|
| 163 | if fs_type == "tmpfs": |
|---|
| 164 | |
|---|
| 165 | continue |
|---|
| 166 | try: |
|---|
| 167 | if os.path.samefile(mount_dir, settings_dir): |
|---|
| 168 | return fields[0] |
|---|
| 169 | except OSError: |
|---|
| 170 | pass |
|---|
| 171 | |
|---|
| 172 | return None |
|---|
| 173 | |
|---|
| 174 | |
|---|
| 175 | def mount_partition(self): |
|---|
| 176 | """Mount a config partition. |
|---|
| 177 | """ |
|---|
| 178 | self.log.debug("trying to mount configuration partition") |
|---|
| 179 | if not self.requires_partition(): |
|---|
| 180 | self.log.warn("mountConfigPartition: configuration partition is " |
|---|
| 181 | + "not required - mounting anyway") |
|---|
| 182 | if self.get_active_partition(): |
|---|
| 183 | self.log.warn("mountConfigPartition: configuration partition already " |
|---|
| 184 | + "mounted - not mounting again") |
|---|
| 185 | return False |
|---|
| 186 | conf_partitions = self.get_available_partitions() |
|---|
| 187 | mount_dir = self.prefs["Locations"]["SettingsDir"] |
|---|
| 188 | if not conf_partitions: |
|---|
| 189 | |
|---|
| 190 | if os.path.ismount(mount_dir): |
|---|
| 191 | self.log.info("A ramdisk seems to be already mounted as a config " \ |
|---|
| 192 | + "partition - doing nothing ...") |
|---|
| 193 | |
|---|
| 194 | return True |
|---|
| 195 | self.log.warn("no configuration partition found - you have to create " |
|---|
| 196 | + "it first") |
|---|
| 197 | |
|---|
| 198 | |
|---|
| 199 | |
|---|
| 200 | proc = subprocess.Popen( |
|---|
| 201 | shell = False, |
|---|
| 202 | stdout = subprocess.PIPE, |
|---|
| 203 | stderr = subprocess.PIPE, |
|---|
| 204 | args = [ |
|---|
| 205 | self.prefs["Programs"]["super"], |
|---|
| 206 | self.prefs["Programs"]["CryptoBoxRootActions"], |
|---|
| 207 | "program", "mount", |
|---|
| 208 | "_tmpfs_", |
|---|
| 209 | mount_dir ]) |
|---|
| 210 | (stdout, stderr) = proc.communicate() |
|---|
| 211 | if proc.returncode != 0: |
|---|
| 212 | self.log.error("Failed to mount a ramdisk for storing settings: %s" \ |
|---|
| 213 | % stderr) |
|---|
| 214 | return False |
|---|
| 215 | self.log.info("Ramdisk (tmpfs) mounted as config partition ...") |
|---|
| 216 | else: |
|---|
| 217 | partition = conf_partitions[0] |
|---|
| 218 | |
|---|
| 219 | if os.path.ismount(mount_dir): |
|---|
| 220 | self.umount_partition() |
|---|
| 221 | proc = subprocess.Popen( |
|---|
| 222 | shell = False, |
|---|
| 223 | stdout = subprocess.PIPE, |
|---|
| 224 | stderr = subprocess.PIPE, |
|---|
| 225 | args = [ |
|---|
| 226 | self.prefs["Programs"]["super"], |
|---|
| 227 | self.prefs["Programs"]["CryptoBoxRootActions"], |
|---|
| 228 | "program", "mount", |
|---|
| 229 | partition, |
|---|
| 230 | mount_dir ]) |
|---|
| 231 | (stdout, stderr) = proc.communicate() |
|---|
| 232 | if proc.returncode != 0: |
|---|
| 233 | self.log.error("Failed to mount the configuration partition (%s): %s" % \ |
|---|
| 234 | (partition, stderr)) |
|---|
| 235 | return False |
|---|
| 236 | self.log.info("configuration partition mounted: %s" % partition) |
|---|
| 237 | |
|---|
| 238 | if self.__is_initialized: |
|---|
| 239 | self.write() |
|---|
| 240 | return True |
|---|
| 241 | |
|---|
| 242 | |
|---|
| 243 | def umount_partition(self): |
|---|
| 244 | """Umount the currently active configuration partition. |
|---|
| 245 | """ |
|---|
| 246 | mount_dir = self.prefs["Locations"]["SettingsDir"] |
|---|
| 247 | if not os.path.ismount(mount_dir): |
|---|
| 248 | self.log.warn("umountConfigPartition: no configuration partition mounted") |
|---|
| 249 | return False |
|---|
| 250 | self.reload_misc_files() |
|---|
| 251 | proc = subprocess.Popen( |
|---|
| 252 | shell = False, |
|---|
| 253 | stdout = subprocess.PIPE, |
|---|
| 254 | stderr = subprocess.PIPE, |
|---|
| 255 | args = [ |
|---|
| 256 | self.prefs["Programs"]["super"], |
|---|
| 257 | self.prefs["Programs"]["CryptoBoxRootActions"], |
|---|
| 258 | "program", "umount", |
|---|
| 259 | mount_dir ]) |
|---|
| 260 | (stdout, stderr) = proc.communicate() |
|---|
| 261 | if proc.returncode != 0: |
|---|
| 262 | self.log.error("Failed to unmount the configuration partition: %s" % stderr) |
|---|
| 263 | return False |
|---|
| 264 | self.log.info("configuration partition unmounted") |
|---|
| 265 | return True |
|---|
| 266 | |
|---|
| 267 | |
|---|
| 268 | def get_available_partitions(self): |
|---|
| 269 | """returns a sequence of found config partitions""" |
|---|
| 270 | self.log.debug("Retrieving available configuration partitions ...") |
|---|
| 271 | proc = subprocess.Popen( |
|---|
| 272 | shell = False, |
|---|
| 273 | stdout = subprocess.PIPE, |
|---|
| 274 | stderr = subprocess.PIPE, |
|---|
| 275 | args = [ |
|---|
| 276 | self.prefs["Programs"]["blkid"], |
|---|
| 277 | "-c", os.path.devnull, |
|---|
| 278 | "-t", "LABEL=%s" % self.prefs["Main"]["ConfigVolumeLabel"] ]) |
|---|
| 279 | (output, error) = proc.communicate() |
|---|
| 280 | if proc.returncode == 2: |
|---|
| 281 | self.log.info("No configuration partitions found") |
|---|
| 282 | return [] |
|---|
| 283 | elif proc.returncode == 4: |
|---|
| 284 | self.log.warn("Failed to call 'blkid' for unknown reasons.") |
|---|
| 285 | return [] |
|---|
| 286 | elif proc.returncode == 0: |
|---|
| 287 | if output: |
|---|
| 288 | return [e.strip().split(":", 1)[0] for e in output.splitlines()] |
|---|
| 289 | else: |
|---|
| 290 | return [] |
|---|
| 291 | else: |
|---|
| 292 | self.log.warn("Unknown exit code of 'blkid': %d - %s" \ |
|---|
| 293 | % (proc.returncode, error)) |
|---|
| 294 | |
|---|
| 295 | |
|---|
| 296 | def prepare_partition(self): |
|---|
| 297 | """Mount a config partition if necessary. |
|---|
| 298 | """ |
|---|
| 299 | if self.requires_partition() and not self.get_active_partition(): |
|---|
| 300 | self.mount_partition() |
|---|
| 301 | |
|---|
| 302 | |
|---|
| 303 | def __getitem__(self, key): |
|---|
| 304 | """redirect all requests to the 'prefs' attribute""" |
|---|
| 305 | return self.prefs[key] |
|---|
| 306 | |
|---|
| 307 | |
|---|
| 308 | def __get_preferences(self, config_file): |
|---|
| 309 | """Load the CryptoBox configuration. |
|---|
| 310 | """ |
|---|
| 311 | import StringIO |
|---|
| 312 | config_rules = StringIO.StringIO(self.validation_spec) |
|---|
| 313 | try: |
|---|
| 314 | prefs = configobj.ConfigObj(config_file, configspec=config_rules) |
|---|
| 315 | if prefs: |
|---|
| 316 | self.log.info("found config: %s" % prefs.items()) |
|---|
| 317 | else: |
|---|
| 318 | raise CBConfigUnavailableError( |
|---|
| 319 | "failed to load the config file: %s" % config_file) |
|---|
| 320 | except IOError, err_msg: |
|---|
| 321 | raise CBConfigUnavailableError( |
|---|
| 322 | "unable to open the config file (%s): %s" % \ |
|---|
| 323 | (config_file, err_msg)) |
|---|
| 324 | except configobj.ConfigObjError, err_msg: |
|---|
| 325 | raise CBConfigError("failed to load config file (%s): %s" % \ |
|---|
| 326 | (config_file, err_msg)) |
|---|
| 327 | return prefs |
|---|
| 328 | |
|---|
| 329 | |
|---|
| 330 | def __validate_config(self): |
|---|
| 331 | """Check the configuration settings and cast value types. |
|---|
| 332 | """ |
|---|
| 333 | result = self.prefs.validate(CryptoBoxSettingsValidator(), preserve_errors=True) |
|---|
| 334 | error_list = configobj.flatten_errors(self.prefs, result) |
|---|
| 335 | if not error_list: |
|---|
| 336 | return |
|---|
| 337 | error_msgs = [] |
|---|
| 338 | for sections, key, text in error_list: |
|---|
| 339 | section_name = "->".join(sections) |
|---|
| 340 | if not text: |
|---|
| 341 | error_msg = "undefined configuration value (%s) in section '%s'" % \ |
|---|
| 342 | (key, section_name) |
|---|
| 343 | else: |
|---|
| 344 | error_msg = "invalid configuration value (%s) in section '%s': %s" % \ |
|---|
| 345 | (key, section_name, text) |
|---|
| 346 | error_msgs.append(error_msg) |
|---|
| 347 | raise CBConfigError, "\n".join(error_msgs) |
|---|
| 348 | |
|---|
| 349 | |
|---|
| 350 | def __check_unknown_preferences(self): |
|---|
| 351 | """Check the configuration file for unknown settings to avoid spelling mistakes. |
|---|
| 352 | """ |
|---|
| 353 | import StringIO |
|---|
| 354 | config_rules = configobj.ConfigObj(StringIO.StringIO(self.validation_spec), |
|---|
| 355 | list_values=False) |
|---|
| 356 | self.__recursive_section_check("", self.prefs, config_rules) |
|---|
| 357 | |
|---|
| 358 | |
|---|
| 359 | def __recursive_section_check(self, section_path, section_config, section_rules): |
|---|
| 360 | """should be called by '__check_unknown_preferences' for every section |
|---|
| 361 | sends a warning message to the logger for every undefined (see validation_spec) |
|---|
| 362 | configuration setting |
|---|
| 363 | """ |
|---|
| 364 | for section in section_config.keys(): |
|---|
| 365 | element_path = section_path + section |
|---|
| 366 | if section in section_rules.keys(): |
|---|
| 367 | if isinstance(section_config[section], configobj.Section): |
|---|
| 368 | if isinstance(section_rules[section], configobj.Section): |
|---|
| 369 | self.__recursive_section_check(element_path + "->", |
|---|
| 370 | section_config[section], section_rules[section]) |
|---|
| 371 | else: |
|---|
| 372 | self.log.warn("configuration setting should be a value " |
|---|
| 373 | + "instead of a section name: %s" % element_path) |
|---|
| 374 | else: |
|---|
| 375 | if not isinstance(section_rules[section], configobj.Section): |
|---|
| 376 | pass |
|---|
| 377 | else: |
|---|
| 378 | self.log.warn("configuration setting should be a section " |
|---|
| 379 | + "name instead of a value: %s" % element_path) |
|---|
| 380 | elif element_path.startswith("PluginSettings->"): |
|---|
| 381 | |
|---|
| 382 | pass |
|---|
| 383 | else: |
|---|
| 384 | self.log.warn("unknown configuration setting: %s" % element_path) |
|---|
| 385 | |
|---|
| 386 | |
|---|
| 387 | def __get_plugin_config(self): |
|---|
| 388 | """Load the plugin configuration file if it exists. |
|---|
| 389 | """ |
|---|
| 390 | import StringIO |
|---|
| 391 | plugin_rules = StringIO.StringIO(self.pluginValidationSpec) |
|---|
| 392 | try: |
|---|
| 393 | try: |
|---|
| 394 | plugin_conf_file = os.path.join( |
|---|
| 395 | self.prefs["Locations"]["SettingsDir"], PLUGINCONF_FILE) |
|---|
| 396 | except KeyError: |
|---|
| 397 | raise CBConfigUndefinedError("Locations", "SettingsDir") |
|---|
| 398 | except SyntaxError: |
|---|
| 399 | raise CBConfigInvalidValueError("Locations", "SettingsDir", plugin_conf_file, |
|---|
| 400 | "failed to interprete the filename of the plugin config file " |
|---|
| 401 | + "correctly (%s)" % plugin_conf_file) |
|---|
| 402 | |
|---|
| 403 | if os.path.exists(plugin_conf_file): |
|---|
| 404 | plugin_conf = configobj.ConfigObj(plugin_conf_file, configspec=plugin_rules) |
|---|
| 405 | else: |
|---|
| 406 | try: |
|---|
| 407 | plugin_conf = configobj.ConfigObj(plugin_conf_file, |
|---|
| 408 | configspec=plugin_rules, create_empty=True) |
|---|
| 409 | except IOError: |
|---|
| 410 | plugin_conf = configobj.ConfigObj(configspec=plugin_rules) |
|---|
| 411 | plugin_conf.filename = plugin_conf_file |
|---|
| 412 | |
|---|
| 413 | plugin_conf.validate(validate.Validator()) |
|---|
| 414 | return plugin_conf |
|---|
| 415 | |
|---|
| 416 | |
|---|
| 417 | def __get_volumes_database(self): |
|---|
| 418 | """Load the volume database file if it exists. |
|---|
| 419 | """ |
|---|
| 420 | |
|---|
| 421 | try: |
|---|
| 422 | try: |
|---|
| 423 | conf_file = os.path.join( |
|---|
| 424 | self.prefs["Locations"]["SettingsDir"], VOLUMESDB_FILE) |
|---|
| 425 | except KeyError: |
|---|
| 426 | raise CBConfigUndefinedError("Locations", "SettingsDir") |
|---|
| 427 | except SyntaxError: |
|---|
| 428 | raise CBConfigInvalidValueError("Locations", "SettingsDir", conf_file, |
|---|
| 429 | "failed to interprete the filename of the volume database " |
|---|
| 430 | + "correctly (%s)" % conf_file) |
|---|
| 431 | |
|---|
| 432 | if os.path.exists(conf_file): |
|---|
| 433 | conf = configobj.ConfigObj(conf_file) |
|---|
| 434 | else: |
|---|
| 435 | try: |
|---|
| 436 | conf = configobj.ConfigObj(conf_file, create_empty=True) |
|---|
| 437 | except IOError: |
|---|
| 438 | conf = configobj.ConfigObj() |
|---|
| 439 | conf.filename = conf_file |
|---|
| 440 | return conf |
|---|
| 441 | |
|---|
| 442 | |
|---|
| 443 | def __get_user_db(self): |
|---|
| 444 | """Load the user database file if it exists. |
|---|
| 445 | """ |
|---|
| 446 | import StringIO |
|---|
| 447 | try: |
|---|
| 448 | |
|---|
| 449 | import hashlib |
|---|
| 450 | get_hash_obj = lambda text: hashlib.sha1(text) |
|---|
| 451 | except ImportError: |
|---|
| 452 | |
|---|
| 453 | import sha |
|---|
| 454 | get_hash_obj = lambda text: sha.new(text) |
|---|
| 455 | user_db_rules = StringIO.StringIO(self.userDatabaseSpec) |
|---|
| 456 | try: |
|---|
| 457 | try: |
|---|
| 458 | user_db_file = os.path.join( |
|---|
| 459 | self.prefs["Locations"]["SettingsDir"], USERDB_FILE) |
|---|
| 460 | except KeyError: |
|---|
| 461 | raise CBConfigUndefinedError("Locations", "SettingsDir") |
|---|
| 462 | except SyntaxError: |
|---|
| 463 | raise CBConfigInvalidValueError("Locations", "SettingsDir", user_db_file, |
|---|
| 464 | "failed to interprete the filename of the users database file " |
|---|
| 465 | + "correctly (%s)" % user_db_file) |
|---|
| 466 | |
|---|
| 467 | if os.path.exists(user_db_file): |
|---|
| 468 | user_db = configobj.ConfigObj(user_db_file, configspec=user_db_rules) |
|---|
| 469 | else: |
|---|
| 470 | try: |
|---|
| 471 | user_db = configobj.ConfigObj(user_db_file, |
|---|
| 472 | configspec=user_db_rules, create_empty=True) |
|---|
| 473 | except IOError: |
|---|
| 474 | user_db = configobj.ConfigObj(configspec=user_db_rules) |
|---|
| 475 | user_db.filename = user_db_file |
|---|
| 476 | |
|---|
| 477 | user_db.validate(validate.Validator()) |
|---|
| 478 | |
|---|
| 479 | user_db.get_digest = lambda password: get_hash_obj(password).hexdigest() |
|---|
| 480 | return user_db |
|---|
| 481 | |
|---|
| 482 | |
|---|
| 483 | def __get_misc_files(self): |
|---|
| 484 | """Load miscelleanous configuration files. |
|---|
| 485 | |
|---|
| 486 | e.g.: an ssl certificate, ... |
|---|
| 487 | """ |
|---|
| 488 | misc_dir = os.path.join(self.prefs["Locations"]["SettingsDir"], "misc") |
|---|
| 489 | if (not os.path.isdir(misc_dir)) or (not os.access(misc_dir, os.X_OK)): |
|---|
| 490 | return [] |
|---|
| 491 | misc_files = [] |
|---|
| 492 | for root, dirs, files in os.walk(misc_dir): |
|---|
| 493 | misc_files.extend([os.path.join(root, e) for e in files]) |
|---|
| 494 | return [MiscConfigFile(os.path.join(misc_dir, f), self.log) for f in misc_files] |
|---|
| 495 | |
|---|
| 496 | |
|---|
| 497 | def __get_config_filename(self, config_file): |
|---|
| 498 | """Search for the configuration file. |
|---|
| 499 | """ |
|---|
| 500 | import types |
|---|
| 501 | if config_file is None: |
|---|
| 502 | |
|---|
| 503 | conf_file_list = [os.path.expanduser(f) |
|---|
| 504 | for f in CONF_LOCATIONS |
|---|
| 505 | if os.path.exists(os.path.expanduser(f))] |
|---|
| 506 | if not conf_file_list: |
|---|
| 507 | |
|---|
| 508 | raise CBConfigUnavailableError() |
|---|
| 509 | config_file = conf_file_list[0] |
|---|
| 510 | else: |
|---|
| 511 | |
|---|
| 512 | if type(config_file) != types.StringType: |
|---|
| 513 | raise CBConfigUnavailableError( |
|---|
| 514 | "invalid config file specified: %s" % config_file) |
|---|
| 515 | if not os.path.exists(config_file): |
|---|
| 516 | raise CBConfigUnavailableError( |
|---|
| 517 | "could not find the specified configuration file (%s)" % config_file) |
|---|
| 518 | return config_file |
|---|
| 519 | |
|---|
| 520 | |
|---|
| 521 | def __configure_log_handler(self): |
|---|
| 522 | """Configure the log handler of the CryptoBox according to the config. |
|---|
| 523 | """ |
|---|
| 524 | log_level = self.prefs["Log"]["Level"].upper() |
|---|
| 525 | log_level_avail = ["DEBUG", "INFO", "WARN", "ERROR"] |
|---|
| 526 | if not log_level in log_level_avail: |
|---|
| 527 | raise CBConfigInvalidValueError("Log", "Level", log_level, |
|---|
| 528 | "invalid log level: only %s are allowed" % str(log_level_avail)) |
|---|
| 529 | log_destination = self.prefs["Log"]["Destination"].lower() |
|---|
| 530 | |
|---|
| 531 | log_dest_avail = ['file', 'syslog'] |
|---|
| 532 | if not log_destination in log_dest_avail: |
|---|
| 533 | raise CBConfigInvalidValueError("Log", "Destination", log_destination, |
|---|
| 534 | "invalid log destination: only %s are allowed" % str(log_dest_avail)) |
|---|
| 535 | if log_destination == 'file': |
|---|
| 536 | try: |
|---|
| 537 | log_handler = logging.FileHandler(self.prefs["Log"]["Details"]) |
|---|
| 538 | except IOError: |
|---|
| 539 | raise CBEnvironmentError("could not write to log file (%s)" % \ |
|---|
| 540 | self.prefs["Log"]["Details"]) |
|---|
| 541 | log_handler.setFormatter( |
|---|
| 542 | logging.Formatter('%(asctime)s %(levelname)s: %(message)s')) |
|---|
| 543 | elif log_destination == 'syslog': |
|---|
| 544 | log_facility = self.prefs["Log"]["Details"].upper() |
|---|
| 545 | log_facil_avail = ['KERN', 'USER', 'MAIL', 'DAEMON', 'AUTH', 'SYSLOG', |
|---|
| 546 | 'LPR', 'NEWS', 'UUCP', 'CRON', 'AUTHPRIV', 'LOCAL0', 'LOCAL1', |
|---|
| 547 | 'LOCAL2', 'LOCAL3', 'LOCAL4', 'LOCAL5', 'LOCAL6', 'LOCAL7'] |
|---|
| 548 | if not log_facility in log_facil_avail: |
|---|
| 549 | raise CBConfigInvalidValueError("Log", "Details", log_facility, |
|---|
| 550 | "invalid log details for 'syslog': only %s are allowed" % \ |
|---|
| 551 | str(log_facil_avail)) |
|---|
| 552 | |
|---|
| 553 | log_handler = LocalSysLogHandler("CryptoNAS", |
|---|
| 554 | getattr(syslog, 'LOG_%s' % log_facility)) |
|---|
| 555 | log_handler.setFormatter( |
|---|
| 556 | logging.Formatter('%(asctime)s CryptoNAS %(levelname)s: %(message)s')) |
|---|
| 557 | else: |
|---|
| 558 | |
|---|
| 559 | |
|---|
| 560 | raise CBConfigInvalidValueError("Log", "Destination", log_destination, |
|---|
| 561 | "invalid log destination: only %s are allowed" % str(log_dest_avail)) |
|---|
| 562 | cbox_log = logging.getLogger("CryptoNAS") |
|---|
| 563 | |
|---|
| 564 | cbox_log.handlers = [] |
|---|
| 565 | |
|---|
| 566 | cbox_log.addHandler(log_handler) |
|---|
| 567 | |
|---|
| 568 | cbox_log.propagate = False |
|---|
| 569 | |
|---|
| 570 | cbox_log.setLevel(getattr(logging, log_level)) |
|---|
| 571 | |
|---|
| 572 | |
|---|
| 573 | |
|---|
| 574 | |
|---|
| 575 | |
|---|
| 576 | |
|---|
| 577 | |
|---|
| 578 | validation_spec = """ |
|---|
| 579 | [Main] |
|---|
| 580 | AllowedDevices = listOfDevices(default="/dev/invalid") |
|---|
| 581 | DefaultVolumePrefix = string(min=1) |
|---|
| 582 | DefaultCipher = string(default="aes-cbc-essiv:sha256") |
|---|
| 583 | ConfigVolumeLabel = string(min=1, default="cbox_config") |
|---|
| 584 | UseConfigPartition = integer(min=0, max=1, default=0) |
|---|
| 585 | DisabledPlugins = listOfPlugins(default=list()) |
|---|
| 586 | |
|---|
| 587 | [Locations] |
|---|
| 588 | MountParentDir = directoryMountExists(default=None) |
|---|
| 589 | SettingsDir = directorySettingsExists(default=None) |
|---|
| 590 | TemplateDir = directoryTemplateExists(default=None) |
|---|
| 591 | DocDir = directoryDocExists(default=None) |
|---|
| 592 | PluginDir = listOfExistingPluginDirectories(default=None) |
|---|
| 593 | EventDir = string(default="/etc/cryptobox-server/events.d") |
|---|
| 594 | |
|---|
| 595 | [Log] |
|---|
| 596 | Level = option("debug", "info", "warn", "error", default="warn") |
|---|
| 597 | Destination = option("file", "syslog", default="file") |
|---|
| 598 | Details = string(min=1, default="/var/log/cryptobox-server/cryptobox.log") |
|---|
| 599 | |
|---|
| 600 | [WebSettings] |
|---|
| 601 | Stylesheet = string(min=1) |
|---|
| 602 | Languages = listOfLanguages(default="en") |
|---|
| 603 | |
|---|
| 604 | [Programs] |
|---|
| 605 | cryptsetup = fileExecutable(default="/sbin/cryptsetup") |
|---|
| 606 | mkfs = fileExecutable(default="/sbin/mkfs") |
|---|
| 607 | nice = fileExecutable(default="/usr/bin/nice") |
|---|
| 608 | blkid = fileExecutable(default="/sbin/blkid") |
|---|
| 609 | blockdev = fileExecutable(default="/sbin/blockdev") |
|---|
| 610 | mount = fileExecutable(default="/bin/mount") |
|---|
| 611 | umount = fileExecutable(default="/bin/umount") |
|---|
| 612 | super = fileExecutable(default="/usr/bin/super") |
|---|
| 613 | # this is the "program" name as defined in /etc/super.tab |
|---|
| 614 | CryptoBoxRootActions = string(min=1) |
|---|
| 615 | |
|---|
| 616 | [PluginSettings] |
|---|
| 617 | [[__many__]] |
|---|
| 618 | """ |
|---|
| 619 | |
|---|
| 620 | pluginValidationSpec = """ |
|---|
| 621 | [__many__] |
|---|
| 622 | visibility = boolean(default=None) |
|---|
| 623 | requestAuth = boolean(default=None) |
|---|
| 624 | rank = integer(default=None) |
|---|
| 625 | """ |
|---|
| 626 | |
|---|
| 627 | userDatabaseSpec = """ |
|---|
| 628 | [admins] |
|---|
| 629 | admin = string(default=d033e22ae348aeb5660fc2140aec35850c4da997) |
|---|
| 630 | """ |
|---|
| 631 | |
|---|
| 632 | |
|---|
| 633 | class CryptoBoxSettingsValidator(validate.Validator): |
|---|
| 634 | """Some custom configuration check functions. |
|---|
| 635 | """ |
|---|
| 636 | |
|---|
| 637 | def __init__(self): |
|---|
| 638 | validate.Validator.__init__(self) |
|---|
| 639 | self.functions["directoryMountExists"] = \ |
|---|
| 640 | self.check_mount_directory_exists |
|---|
| 641 | self.functions["directorySettingsExists"] = \ |
|---|
| 642 | self.check_settings_directory_exists |
|---|
| 643 | self.functions["directoryTemplateExists"] = \ |
|---|
| 644 | self.check_template_directory_exists |
|---|
| 645 | self.functions["directoryDocExists"] = \ |
|---|
| 646 | self.check_doc_directory_exists |
|---|
| 647 | self.functions["fileExecutable"] = self.check_file_executable |
|---|
| 648 | self.functions["fileWriteable"] = self.check_file_writeable |
|---|
| 649 | self.functions["listOfExistingPluginDirectories"] \ |
|---|
| 650 | = self.check_existing_plugin_directories |
|---|
| 651 | self.functions["listOfLanguages"] = self.list_languages |
|---|
| 652 | self.functions["listOfDevices"] = self.list_devices |
|---|
| 653 | self.functions["listOfPlugins"] = self.list_plugins |
|---|
| 654 | |
|---|
| 655 | |
|---|
| 656 | def check_mount_directory_exists(self, value): |
|---|
| 657 | """Is the mount directory accessible? |
|---|
| 658 | """ |
|---|
| 659 | |
|---|
| 660 | if value is None: |
|---|
| 661 | value = "/var/cache/cryptobox-server/mnt" |
|---|
| 662 | dir_path = os.path.abspath(value) |
|---|
| 663 | if not os.path.isdir(dir_path): |
|---|
| 664 | raise validate.VdtValueError("%s (not found)" % value) |
|---|
| 665 | if not os.access(dir_path, os.X_OK): |
|---|
| 666 | raise validate.VdtValueError("%s (access denied)" % value) |
|---|
| 667 | return dir_path |
|---|
| 668 | |
|---|
| 669 | |
|---|
| 670 | def check_settings_directory_exists(self, value): |
|---|
| 671 | """Is the settings directory accessible? |
|---|
| 672 | """ |
|---|
| 673 | |
|---|
| 674 | if value is None: |
|---|
| 675 | value = "/var/cache/cryptobox-server/settings" |
|---|
| 676 | dir_path = os.path.abspath(value) |
|---|
| 677 | if not os.path.isdir(dir_path): |
|---|
| 678 | raise validate.VdtValueError("%s (not found)" % value) |
|---|
| 679 | if not os.access(dir_path, os.X_OK): |
|---|
| 680 | raise validate.VdtValueError("%s (access denied)" % value) |
|---|
| 681 | return dir_path |
|---|
| 682 | |
|---|
| 683 | |
|---|
| 684 | def check_template_directory_exists(self, value): |
|---|
| 685 | """Is the template directory accessible? |
|---|
| 686 | """ |
|---|
| 687 | |
|---|
| 688 | if value is None: |
|---|
| 689 | value = "/usr/share/cryptobox-server/templates" |
|---|
| 690 | dir_path = os.path.abspath(value) |
|---|
| 691 | if not os.path.isdir(dir_path): |
|---|
| 692 | raise validate.VdtValueError("%s (not found)" % value) |
|---|
| 693 | if not os.access(dir_path, os.X_OK): |
|---|
| 694 | raise validate.VdtValueError("%s (access denied)" % value) |
|---|
| 695 | return dir_path |
|---|
| 696 | |
|---|
| 697 | |
|---|
| 698 | def check_doc_directory_exists(self, value): |
|---|
| 699 | """Is the documentation directory accessible? |
|---|
| 700 | """ |
|---|
| 701 | |
|---|
| 702 | if value is None: |
|---|
| 703 | value = "/usr/share/doc/cryptobox-server/html" |
|---|
| 704 | dir_path = os.path.abspath(value) |
|---|
| 705 | if not os.path.isdir(dir_path): |
|---|
| 706 | raise validate.VdtValueError("%s (not found)" % value) |
|---|
| 707 | if not os.access(dir_path, os.X_OK): |
|---|
| 708 | raise validate.VdtValueError("%s (access denied)" % value) |
|---|
| 709 | return dir_path |
|---|
| 710 | |
|---|
| 711 | |
|---|
| 712 | def check_file_executable(self, value): |
|---|
| 713 | """Is the file executable? |
|---|
| 714 | """ |
|---|
| 715 | file_path = os.path.abspath(value) |
|---|
| 716 | if not os.path.isfile(file_path): |
|---|
| 717 | raise validate.VdtValueError("%s (not found)" % value) |
|---|
| 718 | if not os.access(file_path, os.X_OK): |
|---|
| 719 | raise validate.VdtValueError("%s (access denied)" % value) |
|---|
| 720 | return file_path |
|---|
| 721 | |
|---|
| 722 | |
|---|
| 723 | def check_file_writeable(self, value): |
|---|
| 724 | """Is the file writeable? |
|---|
| 725 | """ |
|---|
| 726 | file_path = os.path.abspath(value) |
|---|
| 727 | if os.path.isfile(file_path): |
|---|
| 728 | if not os.access(file_path, os.W_OK): |
|---|
| 729 | raise validate.VdtValueError("%s (not found)" % value) |
|---|
| 730 | else: |
|---|
| 731 | parent_dir = os.path.dirname(file_path) |
|---|
| 732 | if os.path.isdir(parent_dir) and os.access(parent_dir, os.W_OK): |
|---|
| 733 | return file_path |
|---|
| 734 | raise validate.VdtValueError("%s (directory does not exist)" % value) |
|---|
| 735 | return file_path |
|---|
| 736 | |
|---|
| 737 | |
|---|
| 738 | def check_existing_plugin_directories(self, value): |
|---|
| 739 | """Are these directories accessible? |
|---|
| 740 | """ |
|---|
| 741 | |
|---|
| 742 | if value is None: |
|---|
| 743 | value = ["/usr/share/cryptobox-server/plugins"] |
|---|
| 744 | if not value: |
|---|
| 745 | raise validate.VdtValueError("no plugin directory specified") |
|---|
| 746 | if not isinstance(value, list): |
|---|
| 747 | value = [value] |
|---|
| 748 | result = [] |
|---|
| 749 | for one_dir in value: |
|---|
| 750 | dir_path = os.path.abspath(one_dir) |
|---|
| 751 | if not os.path.isdir(dir_path): |
|---|
| 752 | raise validate.VdtValueError( |
|---|
| 753 | "%s (plugin directory not found)" % one_dir) |
|---|
| 754 | if not os.access(dir_path, os.X_OK): |
|---|
| 755 | raise validate.VdtValueError( |
|---|
| 756 | "%s (access denied for plugin directory)" % one_dir) |
|---|
| 757 | result.append(dir_path) |
|---|
| 758 | return result |
|---|
| 759 | |
|---|
| 760 | def list_languages(self, langs): |
|---|
| 761 | """Return languages as a list. |
|---|
| 762 | """ |
|---|
| 763 | if not langs: |
|---|
| 764 | raise validate.VdtValueError("no language specified") |
|---|
| 765 | if not isinstance(langs, list): |
|---|
| 766 | langs = [langs] |
|---|
| 767 | return langs |
|---|
| 768 | |
|---|
| 769 | def list_devices(self, devices): |
|---|
| 770 | """Return devices as a list. |
|---|
| 771 | """ |
|---|
| 772 | if not devices: |
|---|
| 773 | raise validate.VdtValueError("no device specified") |
|---|
| 774 | if not isinstance(devices, list): |
|---|
| 775 | devices = [devices] |
|---|
| 776 | return devices |
|---|
| 777 | |
|---|
| 778 | def list_plugins(self, plugins): |
|---|
| 779 | """Return plugin names as a list. |
|---|
| 780 | """ |
|---|
| 781 | if not plugins: |
|---|
| 782 | plugins = [] |
|---|
| 783 | if isinstance(plugins, basestring): |
|---|
| 784 | plugins = [plugins] |
|---|
| 785 | elif not isinstance(plugins, list): |
|---|
| 786 | raise validate.VdtValueError("invalid list of disabled plugins") |
|---|
| 787 | return plugins |
|---|
| 788 | |
|---|
| 789 | |
|---|
| 790 | |
|---|
| 791 | class MiscConfigFile: |
|---|
| 792 | """all other config files (e.g. a ssl certificate) to be stored""" |
|---|
| 793 | |
|---|
| 794 | maxSize = 20480 |
|---|
| 795 | |
|---|
| 796 | def __init__(self, filename, logger): |
|---|
| 797 | self.filename = filename |
|---|
| 798 | self.log = logger |
|---|
| 799 | self.content = None |
|---|
| 800 | self.load() |
|---|
| 801 | |
|---|
| 802 | |
|---|
| 803 | def load(self): |
|---|
| 804 | """Load a configuration file into memory. |
|---|
| 805 | """ |
|---|
| 806 | fdesc = open(self.filename, "rb") |
|---|
| 807 | |
|---|
| 808 | self.content = fdesc.read(self.maxSize) |
|---|
| 809 | if fdesc.tell() == self.maxSize: |
|---|
| 810 | self.log.warn("file in misc settings directory (" + str(self.filename) \ |
|---|
| 811 | + ") is bigger than allowed (" + str(self.maxSize) + ")") |
|---|
| 812 | fdesc.close() |
|---|
| 813 | |
|---|
| 814 | |
|---|
| 815 | def save(self): |
|---|
| 816 | """Save a configuration file to disk. |
|---|
| 817 | """ |
|---|
| 818 | |
|---|
| 819 | if os.path.exists(self.filename) and not os.access(self.filename, os.W_OK): |
|---|
| 820 | return True |
|---|
| 821 | save_dir = os.path.dirname(self.filename) |
|---|
| 822 | |
|---|
| 823 | if not os.path.isdir(save_dir): |
|---|
| 824 | try: |
|---|
| 825 | os.mkdir(save_dir) |
|---|
| 826 | except IOError: |
|---|
| 827 | return False |
|---|
| 828 | |
|---|
| 829 | try: |
|---|
| 830 | fdesc = open(self.filename, "wb") |
|---|
| 831 | except IOError: |
|---|
| 832 | return False |
|---|
| 833 | try: |
|---|
| 834 | fdesc.write(self.content) |
|---|
| 835 | fdesc.close() |
|---|
| 836 | return True |
|---|
| 837 | except IOError: |
|---|
| 838 | fdesc.close() |
|---|
| 839 | return False |
|---|
| 840 | |
|---|
| 841 | |
|---|
| 842 | |
|---|
| 843 | class LocalSysLogHandler(logging.Handler): |
|---|
| 844 | """Pass logging messages to a local syslog server without unix sockets. |
|---|
| 845 | |
|---|
| 846 | derived from: logging.SysLogHandler |
|---|
| 847 | """ |
|---|
| 848 | |
|---|
| 849 | def __init__(self, prepend='CryptoBox', facility=syslog.LOG_USER): |
|---|
| 850 | logging.Handler.__init__(self) |
|---|
| 851 | self.formatter = None |
|---|
| 852 | self.facility = facility |
|---|
| 853 | syslog.openlog(prepend, 0, facility) |
|---|
| 854 | |
|---|
| 855 | |
|---|
| 856 | def close(self): |
|---|
| 857 | """close the syslog connection |
|---|
| 858 | """ |
|---|
| 859 | syslog.closelog() |
|---|
| 860 | logging.Handler.close(self) |
|---|
| 861 | |
|---|
| 862 | |
|---|
| 863 | def emit(self, record): |
|---|
| 864 | """format and send the log message |
|---|
| 865 | """ |
|---|
| 866 | msg = "%s: %s" % (record.levelname, record.getMessage()) |
|---|
| 867 | try: |
|---|
| 868 | syslog.syslog(record.levelno, msg) |
|---|
| 869 | except Exception: |
|---|
| 870 | self.handleError(record) |
|---|