#!/usr/bin/env python2.4 ''' This is the web interface for a fileserver managing encrypted filesystems. It was originally written in bash/perl. Now a complete rewrite is in progress. So things might be confusing here. Hopefully not for long. :) ''' # check python version import sys (ver_major, ver_minor, ver_sub, ver_desc, ver_subsub) = sys.version_info if (ver_major < 2) or ((ver_major == 2) and (ver_minor < 4)): sys.stderr.write("You need a python version >= 2.4\nCurrent version is:\n %s\n" % sys.version) sys.exit(1) import CryptoBoxContainer import types import re import os import unittest import logging import subprocess from CryptoBoxExceptions import * CONF_LOCATIONS = [ "./cryptobox.conf", "~/.cryptobox.conf", "/etc/cryptobox/cryptobox.conf"] class CryptoBox: '''this class rules them all! put things like logging, conf and oter stuff in here, that might be used by more classes, it will be passed on to them''' def __init__(self, config_file=None): self.__initLogging() self.__initPreferences(config_file) self.__runTests() def __initLogging(self): import logging '''initialises the logging system use it with: 'self.log.[debug|info|warning|error|critical](logmessage)' all classes should get the logging instance during __init__: self.log = logging.getLogger("CryptoBox") first we output all warnings/errors to stderr as soon as we opened the config file successfully, we redirect debug output to the configured destination''' ## basicConfig(...) needs python >= 2.4 try: self.log = logging.getLogger("CryptoBox") logging.basicConfig( format='%(asctime)s %(module)s %(levelname)s %(message)s', stderr=sys.stderr) self.log.setLevel(logging.ERROR) self.log.info("loggingsystem is up'n running") ## from now on everything can be logged via self.log... except: raise CBEnvironmentError("couldn't initialise the loggingsystem. I give up.") def __initPreferences(self, config_file): try: import configobj ## needed for reading and writing of the config file except: raise CBEnvironmentError("couldn't import 'configobj'! Try 'apt-get install python-configobj'.") # search for the configuration file if config_file is None: # no config file was specified - we will look for it in the ususal locations conf_file_list = [os.path.expanduser(f) for f in CONF_LOCATIONS if os.path.exists(os.path.expanduser(f))] if not conf_file_list: # no possible config file found in the usual locations raise CBConfigUnavailableError() config_file = conf_file_list[0] else: # a config file was specified (e.g. via command line) if type(config_file) != types.StringType: raise CBConfigUnavailableError("invalid config file specified: %s" % config_file) if not os.path.exists(config_file): raise CBConfigUnavailableError("could not find the specified configuration file (%s)" % config_file) try: self.cbxPrefs = configobj.ConfigObj(config_file) if self.cbxPrefs: self.log.info("found config: %s" % self.cbxPrefs.items()) else: raise CBConfigUnavailableError("failed to load the config file: %s" % config_file) except IOError: raise CBConfigUnavailableError("unable to open the config file: %s" % config_file) try: try: nameDB_file = self.cbxPrefs["Locations"]["NameDatabase"] except KeyError: raise CBConfigUndefinedError("Locations", "NameDatabase") except SyntaxError: raise CBConfigInvalidValueError("Locations", "NameDatabase", nameDB_file, "failed to interprete the filename of the name database correctly") ## create nameDB is necessary if os.path.exists(nameDB_file): self.nameDB = configobj.ConfigObj(nameDB_file) else: self.nameDB = configobj.ConfigObj(nameDB_file, create_empty=True) ## check if nameDB file was created successfully? if not os.path.exists(nameDB_file): raise CBEnvironmentError("failed to create name database (%s)" % nameDB_file) # get the loglevel try: log_level = self.cbxPrefs["Log"]["Level"].upper() log_level_avail = ["DEBUG", "INFO", "WARN", "ERROR"] if not log_level in log_level_avail: raise TypeError except KeyError: raise CBConfigUndefinedError("Log", "Level") except TypeError: raise CBConfigInvalidValueError("Log", "Level", log_level, "invalid log level: only %s are allowed" % log_level_avail) try: try: new_handler = logging.FileHandler(self.cbxPrefs["Log"]["Details"]) except KeyError: raise CBConfigUndefinedError("Log", "Details") except IOError: raise CBEnvironmentError("could not create the log file (%s)" % self.cbxPrefs["Log"]["Details"]) new_handler.setFormatter(logging.Formatter('%(asctime)s %(module)s %(levelname)s: %(message)s')) self.log.addHandler(new_handler) ## do not call parent's handlers self.log.propagate = False ## 'log_level' is a string -> use 'getattr' self.log.setLevel(getattr(logging,log_level)) # do some initial checks def __runTests(self): self.__runTestRootPriv() self.__runTestLocations() def __runTestLocations(self): """check if all configured locations exist""" try: conf_locations = self.cbxPrefs["Locations"] except KeyError: raise CBConfigUndefinedError("Locations") try: for key in ["MountParentDir", "NameDatabase", "TemplateDir", "LangDir", "DocDir"]: value = self.cbxPrefs["Locations"][key] #if not (os.path.exists(value) and os.access(value, os.R_OK)): if not os.access(value, os.R_OK): raise CBConfigInvalidValueError("Locations", key, value, "could not access") except KeyError: raise CBConfigUndefinedError("Locations", key) def __runTestRootPriv(self): """try to run 'super' with 'CryptoBoxRootActions'""" try: devnull = open(os.devnull, "w") except IOError: raise CBEnvironmentError("could not open %s for writing!" % os.devnull) try: prog_super = self.cbxPrefs["Programs"]["super"] except KeyError: raise CBConfigUndefinedError("Programs", "super") try: prog_rootactions = self.cbxPrefs["Programs"]["CryptoBoxRootActions"] except KeyError: raise CBConfigUndefinedError("Programs", "CryptoBoxRootActions") try: proc = subprocess.Popen( shell = False, stdout = devnull, stderr = devnull, args = [prog_super, prog_rootactions, "check"]) except OSError: raise CBEnvironmentError("failed to execute 'super' (%s)" % self.cbxPrefs["Programs"]["super"]) proc.wait() if proc.returncode != 0: raise CBEnvironmentError("failed to call CryptoBoxRootActions (%s) via 'super' - maybe you did not add the appropriate line to /etc/super.tab?" % prog_rootactions) # this method just demonstrates inheritance effects - may be removed def cbx_inheritance_test(self, string="you lucky widow"): self.log.info(string) # RFC: why should CryptoBoxProps inherit CryptoBox? [l] # RFC: shouldn't we move all useful functions of CryptoBoxProps to CryptoBox? [l] class CryptoBoxProps(CryptoBox): '''Get and set the properties of a CryptoBox This class contains all available devices that may be accessed. All properties of the cryptobox can be accessed by this class. ''' def __init__(self, config_file=None): '''read config and fill class variables''' CryptoBox.__init__(self, config_file) #self.cbx_inheritance_test() #print self.cbxPrefs.items() #### self.containers = [] for device in self.__getAvailablePartitions(): if self.isDeviceAllowed(device): self.containers.append(CryptoBoxContainer.CryptoBoxContainer(device, self)) def isDeviceAllowed(self, devicename): "check if a device is white-listed for being used as cryptobox containers" allowed = self.cbxPrefs["Main"]["AllowedDevices"] if type(allowed) == types.StringType: allowed = [allowed] for a_dev in allowed: "remove double dots and so on ..." real_device = os.path.realpath(devicename) if a_dev and re.search('^' + a_dev, real_device): return True return False def getLogData(self, lines=None, maxSize=None): """get the most recent log entries of the cryptobox the maximum number and size of these entries can be limited by 'lines' and 'maxSize' """ # return nothing if the currently selected log output is not a file try: if self.cbxPrefs["Log"]["Destination"].upper() != "FILE": return [] log_file = self.cbxPrefs["Log"]["Details"] except KeyError: self.log.error("could not evaluate one of the following config settings: [Log]->Destination or [Log]->Details") return [] try: fd = open(log_file, "r") if maxSize: content = fd.readlines(maxSize) else: content = fd.readlines() fd.close() except IOError: self.log.warn("failed to read the log file (%s)" % log_file) return [] if lines: content = content[-lines:] return content def getContainerList(self, filterType=None, filterName=None): "retrieve the list of all containers of this cryptobox" try: result = self.containers[:] if filterType != None: if filterType in range(len(CryptoBoxContainer.Types)): return [e for e in self.containers if e.getType() == filterType] else: self.log.info("invalid filterType (%d)" % filterType) result.clear() if filterName != None: result = [e for e in self.containers if e.getName() == filterName] return result except AttributeError: return [] def setNameForUUID(self, uuid, name): "assign a name to a uuid in the ContainerNameDatabase" used_uuid = self.getUUIDForName(name) "first remove potential conflicting uuid/name combination" if used_uuid: del self.nameDB[used_uuid] self.nameDB[uuid] = name self.nameDB.write() def getNameForUUID(self, uuid): "get the name belonging to a specified key (usually the UUID of a fs)" try: return self.nameDB[uuid] except KeyError: return None def getUUIDForName(self, name): """ get the key belonging to a value in the ContainerNameDatabase this is the reverse action of 'getNameForUUID' """ for key in self.nameDB.keys(): if self.nameDB[key] == name: return key "the uuid was not found" return None """ ************ internal stuff starts here *********** """ def __getAvailablePartitions(self): "retrieve a list of all available containers" ret_list = [] try: "the following reads all lines of /proc/partitions and adds the mentioned devices" fpart = open("/proc/partitions", "r") try: line = fpart.readline() while line: p_details = line.split() if (len(p_details) == 4): "the following code prevents double entries like /dev/hda and /dev/hda1" (p_major, p_minor, p_size, p_device) = p_details if re.search('^[0-9]*$', p_major) and re.search('^[0-9]*$', p_minor): p_parent = re.sub('[1-9]?[0-9]$', '', p_device) if p_parent == p_device: if [e for e in ret_list if re.search('^' + p_parent + '[1-9]?[0-9]$', e)]: "major partition - its children are already in the list" pass else: "major partition - but there are no children for now" ret_list.append(p_device) else: "minor partition - remove parent if necessary" if p_parent in ret_list: ret_list.remove(p_parent) ret_list.append(p_device) line = fpart.readline() finally: fpart.close() return [self.__getAbsoluteDeviceName(e) for e in ret_list] except IOError: self.log.warning("Could not read /proc/partitions") return [] def __getAbsoluteDeviceName(self, shortname): """ returns the absolute file name of a device (e.g.: "hda1" -> "/dev/hda1") this does also work for device mapper devices if the result is non-unique, one arbitrary value is returned""" if re.search('^/', shortname): return shortname default = os.path.join("/dev", shortname) if os.path.exists(default): return default result = self.__findMajorMinorOfDevice(shortname) "if no valid major/minor was found -> exit" if not result: return default (major, minor) = result "for device-mapper devices (major == 254) ..." if major == 254: result = self.__findMajorMinorDeviceName("/dev/mapper", major, minor) if result: return result[0] "now check all files in /dev" result = self.__findMajorMinorDeviceName("/dev", major, minor) if result: return result[0] return default def __findMajorMinorOfDevice(self, device): "return the major/minor numbers of a block device by querying /sys/block/?/dev" if not os.path.exists(os.path.join("/sys/block", device)): return None blockdev_info_file = os.path.join(os.path.join("/sys/block", device), "dev") try: f_blockdev_info = open(blockdev_info_file, "r") blockdev_info = f_blockdev_info.read() f_blockdev_info.close() (str_major, str_minor) = blockdev_info.split(":") "numeric conversion" try: major = int(str_major) minor = int(str_minor) return (major, minor) except ValueError: "unknown device numbers -> stop guessing" return None except IOError: pass def __findMajorMinorDeviceName(self, dir, major, minor): "returns the names of devices with the specified major and minor number" collected = [] try: subdirs = [os.path.join(dir, e) for e in os.listdir(dir) if (not os.path.islink(os.path.join(dir, e))) and os.path.isdir(os.path.join(dir, e))] "do a recursive call to parse the directory tree" for dirs in subdirs: collected.extend(self.__findMajorMinorDeviceName(dirs, major, minor)) "filter all device inodes in this directory" collected.extend([os.path.realpath(os.path.join(dir, e)) for e in os.listdir(dir) if (os.major(os.stat(os.path.join(dir, e)).st_rdev) == major) and (os.minor(os.stat(os.path.join(dir, e)).st_rdev) == minor)]) result = [] for e in collected: if e not in result: result.append(e) return collected except OSError: return [] if __name__ == "__main__": cb = CryptoBox()