From ad3de60dd15198b3e7ed14c99cc7ed24aec54c08 Mon Sep 17 00:00:00 2001 From: lars Date: Wed, 24 Jan 2007 02:51:17 +0000 Subject: [PATCH] added "missing dependency" warnings to network, data, encrypted_webinterface and partition plugins add "self.root_action" to the plugin specification implement non-interactive key certificate creation during startup if necessary (the produced certificate is still broken) run stunnel during startup returned environment warnings are expected to be lists --- bin/run_webserver.sh | 2 +- plugins/date/date.py | 11 ++ plugins/date/language.hdf | 8 + .../encrypted_webinterface.py | 164 ++++++++++++++++-- plugins/encrypted_webinterface/language.hdf | 12 +- plugins/encrypted_webinterface/root_action.py | 30 +++- plugins/network/language.hdf | 13 ++ plugins/network/network.py | 21 ++- plugins/partition/language.hdf | 17 ++ plugins/partition/partition.py | 12 +- plugins/plugin-interface.txt | 5 + src/cryptobox/core/settings.py | 18 +- src/cryptobox/plugins/base.py | 9 +- src/cryptobox/web/sites.py | 4 +- 14 files changed, 292 insertions(+), 34 deletions(-) diff --git a/bin/run_webserver.sh b/bin/run_webserver.sh index e250980..46527c7 100755 --- a/bin/run_webserver.sh +++ b/bin/run_webserver.sh @@ -32,5 +32,5 @@ mkdir -p "$BIN_DIR/../ttt/settings" cd "$BIN_DIR" ## run the webserver -"$BIN_DIR/CryptoBoxWebserver" --config="$CONFIG_FILE" --pidfile=/tmp/cryptoboxwebserver.pid --logfile=/tmp/cryptoboxwebser.log --port=8080 --datadir="$BIN_DIR/../www-data" "$@" +"$BIN_DIR/CryptoBoxWebserver" --config="$CONFIG_FILE" --pidfile=/tmp/cryptoboxwebserver.pid --logfile=/tmp/cryptoboxwebserver.log --port=8080 --datadir="$BIN_DIR/../www-data" "$@" diff --git a/plugins/date/date.py b/plugins/date/date.py index 58eee83..e233452 100644 --- a/plugins/date/date.py +++ b/plugins/date/date.py @@ -19,6 +19,10 @@ # """Change date and time. + +requires: + - date + - ntpdate (planned) """ __revision__ = "$Id" @@ -68,6 +72,13 @@ class date(cryptobox.plugins.base.CryptoBoxPlugin): (now.year, now.month, now.day, now.hour, now.minute, now.second) + def get_warnings(self): + warnings = [] + if not os.path.isfile(self.root_action.DATE_BIN): + warnings.append((48, "Plugins.%s.MissingProgramDate" % self.get_name())) + return warnings + + def __prepare_form_data(self): """Set some hdf values. """ diff --git a/plugins/date/language.hdf b/plugins/date/language.hdf index 07f528d..ca743f7 100644 --- a/plugins/date/language.hdf +++ b/plugins/date/language.hdf @@ -37,3 +37,11 @@ WarningMessage { Text = An invalid value for date or time was supplied. Please try again. } } + +EnvironmentWarning { + MissingProgramDate { + Title = Missing program + Text = The program 'date' is not installed. Please ask the administrator of the CryptoBox server to configure it properly. + } +} + diff --git a/plugins/encrypted_webinterface/encrypted_webinterface.py b/plugins/encrypted_webinterface/encrypted_webinterface.py index 7e2ec5c..9223111 100644 --- a/plugins/encrypted_webinterface/encrypted_webinterface.py +++ b/plugins/encrypted_webinterface/encrypted_webinterface.py @@ -19,11 +19,32 @@ # """Create an SSL certificate to encrypt the webinterface connection via stunnel + +requires: + - stunnel (>= 4.0) + - "M2Crypto" python module """ __revision__ = "$Id" import cryptobox.plugins.base +import subprocess +import os +import cherrypy + +CERT_FILENAME = 'cryptobox-ssl-certificate.pem' +KEY_BITS = 409 +CERT_INFOS = { + "C": "SomeCountry", + "ST": "SomeState", + "L": "SomeLocality", + "O": "SomeOrganization", + "OU": "CryptoBox-Server", + "CN": "*", + "emailAddress": ""} +EXPIRE_TIME = 60*60*24*365*20 # 20 years +SIGN_DIGEST = "sha256" +PID_FILE = os.path.join("/tmp/cryptobox-stunnel.pid") class encrypted_webinterface(cryptobox.plugins.base.CryptoBoxPlugin): @@ -50,16 +71,137 @@ class encrypted_webinterface(cryptobox.plugins.base.CryptoBoxPlugin): def get_warnings(self): """check if the connection is encrypted """ - import cherrypy, os - if cherrypy.request.scheme == "https": - return None + warnings = [] + ## check if m2crypto is available + try: + import M2Crypto + except ImportError: + warnings.append((45, "Plugins.%s.MissingModuleM2Crypto" % self.get_name())) + if not os.path.isfile(self.root_action.STUNNEL_BIN): + warnings.append((44, "Plugins.%s.MissingProgramStunnel" % self.get_name())) + ## perform some checks for encrypted connections ## check an environment setting - this is quite common behind proxies - if os.environ.has_key("HTTPS"): - return None - ## this arbitrarily chosen header is documented in README.proxy - if cherrypy.request.headers.has_key("X-SSL-Request") \ - and (cherrypy.request.headers["X-SSL-Request"] == "1"): - return None - ## plaintext connection -> "heavy security risk" (priority=20..39) - return (25, "Plugins.%s.NoSSL" % self.get_name()) + ## the arbitrarily chosen header is documented in README.proxy + if (cherrypy.request.scheme != "https") \ + and (not os.environ.has_key("HTTPS")) \ + and (not (cherrypy.request.headers.has_key("X-SSL-Request") \ + and (cherrypy.request.headers["X-SSL-Request"] == "1"))): + ## plaintext connection -> "heavy security risk" (priority=20..39) + warnings.append((25, "Plugins.%s.NoSSL" % self.get_name())) + return warnings + + + def handle_event(self, event, event_info=None): + """Create a certificate during startup (if it does not exist) and run stunnel + """ + if event == "bootup": + cert_abs_name = self.cbox.prefs.get_misc_config_filename(CERT_FILENAME) + if not os.path.isfile(cert_abs_name): + try: + self.__create_certificate(cert_abs_name) + self.cbox.log.info("Created new SSL certificate: %s" % cert_abs_name) + except IOError, err_msg: + ## do not run stunnel without a certificate + self.cbox.log.warn("Failed to create new SSL certificate (%s): %s" % \ + (cert_abs_name, err_msg)) + return + self.__run_stunnel(cert_abs_name) + elif event == "shutdown": + self.__kill_stunnel() + + + def __kill_stunnel(self): + """try to kill a running stunnel daemon + """ + if not os.path.isfile(PID_FILE): + self.cbox.log.warn("Could not find the pid file of a running stunnel daemon: %s" % PID_FILE) + return + try: + pfile = open(PID_FILE, "r") + try: + pid = pfile.read().strip() + except IOError, err_msg: + self.cbox.log.warn("Failed to read the pid file (%s): %s" % (PID_FILE, err_msg)) + pfile.close() + return + pfile.close() + except IOError, err_msg: + self.cbox.log.warn("Failed to open the pid file (%s): %s" % (PID_FILE, err_msg)) + return + if pid.isdigit(): + pid = int(pid) + else: + return + try: + ## SIGTERM = 15 + os.kill(pid, 15) + except OSError: + pass + + + def __run_stunnel(self, cert_name, dest_port=443): + ## retrieve currently requested port (not necessarily the port served + ## by cherrypy - e.g. in a proxy setup) + request_port = cherrypy.config.get("server.socket_port", 80) + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + args = [ + self.cbox.prefs["Programs"]["super"], + self.cbox.prefs["Programs"]["CryptoBoxRootActions"], + "plugin", os.path.join(self.plugin_dir, "root_action.py"), + cert_name, + str(request_port), + str(dest_port), + PID_FILE ]) + (output, error) = proc.communicate() + if proc.returncode == 0: + self.cbox.log.info("Successfully started 'stunnel'") + return True + else: + self.cbox.log.warn("Failed to run 'stunnel': %s" % error) + return False + + + def __create_certificate(self, filename): + import M2Crypto + import time + string_type = 0x1000 | 1 # see http://www.koders.com/python/.. + # ../fid07A99E089F55187896A06CD4E0B6F21B9B8F5B0B.aspx?s=bavaria + key_gen_number = 0x10001 # commonly used for key generation: 65537 + rsa_key = M2Crypto.RSA.gen_key(KEY_BITS, key_gen_number, callback=lambda: None) + pkey = M2Crypto.EVP.PKey(md=SIGN_DIGEST) + pkey.assign_rsa(rsa_key) + issuer = M2Crypto.X509.X509_Name() + for (key, value) in CERT_INFOS.items(): + issuer.add_entry_by_txt(key, string_type, value, 1, 1, 0) + ## time object + asn_time1 = M2Crypto.ASN1.ASN1_UTCTIME() + asn_time1.set_time(long(time.time()) - EXPIRE_TIME) + asn_time2 = M2Crypto.ASN1.ASN1_UTCTIME() + asn_time2.set_time(long(time.time()) + EXPIRE_TIME) + cert = M2Crypto.X509.X509() + cert.set_issuer(issuer) + cert.set_subject(issuer) + cert.set_pubkey(pkey) + cert.set_not_before(asn_time1) + cert.set_not_after(asn_time2) + cert.sign(pkey, SIGN_DIGEST) + result = "" + result += cert.as_pem() + result += pkey.as_pem(cipher=None) + if not os.path.exists(os.path.dirname(filename)): + os.mkdir(os.path.dirname(filename)) + try: + certfile = open(filename, "w") + except IOError: + raise + try: + certfile.write(result) + except IOError: + certfile.close() + raise + certfile.close() + os.chmod(filename, 0600) diff --git a/plugins/encrypted_webinterface/language.hdf b/plugins/encrypted_webinterface/language.hdf index 9310412..32c4452 100644 --- a/plugins/encrypted_webinterface/language.hdf +++ b/plugins/encrypted_webinterface/language.hdf @@ -11,6 +11,16 @@ EnvironmentWarning { Text = The connection is not encrypted - passwords can be easily intercepted. Link.Text = Use encrypted connection Link.Prot = https - } } + MissingModuleM2Crypto { + Title = Missing module + Text = The python module 'M2Crypto' is missing. It is required for an encrypted connection to the CryptoBox webinterface. Please ask the administrator of the CryptoBox server to install the module. + } + + MissingProgramStunnel { + Title = Missing program + Text = The program 'stunnel' is not installed. Please ask the administrator tof the CryptoBox server to configure it properly. + } +} + diff --git a/plugins/encrypted_webinterface/root_action.py b/plugins/encrypted_webinterface/root_action.py index 2e79bb9..5cc7b79 100755 --- a/plugins/encrypted_webinterface/root_action.py +++ b/plugins/encrypted_webinterface/root_action.py @@ -30,7 +30,17 @@ STUNNEL_BIN = "/usr/bin/stunnel" import sys import os -def run_stunnel(cert_file, src_port, dst_port): + +def _get_username(uid): + import pwd + try: + user_entry = pwd.getpwuid(uid) + except KeyError: + return False + return user_entry[0] + + +def run_stunnel(cert_file, src_port, dst_port, pid_file): import subprocess if not src_port.isdigit(): sys.stderr.write("Source port is not a number: %s" % src_port) @@ -38,15 +48,21 @@ def run_stunnel(cert_file, src_port, dst_port): if not dst_port.isdigit(): sys.stderr.write("Destination port is not a number: %s" % dst_port) return False - if not os.path.isfile(cert_file, src_port, dst_port): + if not os.path.isfile(cert_file): sys.stderr.write("The certificate file (%s) does not exist!" % cert_file) return False + username = _get_username(os.getuid()) + if not username: + sys.stderr.write("Could not retrieve the username with uid=%d." % os.getuid()) + return False proc = subprocess.Popen( shell = False, args = [ STUNNEL_BIN, + "-P", pid_file, "-p", cert_file, "-d", dst_port, - "-r", src_port ]) + "-r", src_port, + "-s", username ]) proc.wait() return proc.returncode == 0 @@ -56,13 +72,13 @@ if __name__ == "__main__": self_bin = sys.argv[0] - if len(args) != 3: + if len(args) != 4: sys.stderr.write("%s: invalid number of arguments (%d instead of %d))\n" % \ - (self_bin, len(args), 3)) + (self_bin, len(args), 4)) sys.exit(1) - if not run_stunnel(args[0], args[1], args[2]): - sys.stderr.write("%s: failed to run 'stunnel'!") + if not run_stunnel(args[0], args[1], args[2], args[3]): + sys.stderr.write("%s: failed to run 'stunnel'!" % self_bin) sys.exit(100) sys.exit(0) diff --git a/plugins/network/language.hdf b/plugins/network/language.hdf index aa0bb94..5a6edac 100644 --- a/plugins/network/language.hdf +++ b/plugins/network/language.hdf @@ -27,3 +27,16 @@ SuccessMessage { Text = The network address has been changed. In a few seconds you will get redirected to the new address. } } + +EnvironmentWarning { + MissingProgramIfconfig { + Title = Missing program + Text = The 'ifconfig' program is not installed. Please ask the administrator of the CryptoBox server to install it. + } + + MissingProgramRoute { + Title = Missing program + Text = The 'route' program is not installed. Please ask the administrator of the CryptoBox server to configure it properly. + } +} + diff --git a/plugins/network/network.py b/plugins/network/network.py index 090ee41..07aafb5 100644 --- a/plugins/network/network.py +++ b/plugins/network/network.py @@ -19,6 +19,10 @@ # """The network feature of the CryptoBox. + +requires: + - ifconfig + - route """ __revision__ = "$Id" @@ -116,6 +120,17 @@ class network(cryptobox.plugins.base.CryptoBoxPlugin): self.__set_ip(self.prefs["_address"]) + def get_warnings(self): + """Check for missing programs + """ + warnings = [] + if not os.path.isfile(self.root_action.IFCONFIG_BIN): + warnings.append((55, "MissingProgramIfconfig")) + if not os.path.isfile(self.root_action.GWCONFIG_BIN): + warnings.append((52, "Plugins.%s.MissingProgramRoute" % self.get_name())) + return warnings + + def __get_redirect_destination(self, ip): """Put the new URL together. """ @@ -142,16 +157,12 @@ class network(cryptobox.plugins.base.CryptoBoxPlugin): """Retrieve the current IP. """ import re - import imp - ## load some values from the root_action.py script - root_action_plug = imp.load_source("root_action", - os.path.join(self.plugin_dir, "root_action.py")) ## get the current IP of the network interface proc = subprocess.Popen( shell = False, stdout = subprocess.PIPE, args = [ - root_action_plug.IFCONFIG_BIN, + self.root_action.IFCONFIG_BIN, self.__get_interface()]) (stdout, stderr) = proc.communicate() if proc.returncode != 0: diff --git a/plugins/partition/language.hdf b/plugins/partition/language.hdf index 1fe1f52..da95549 100644 --- a/plugins/partition/language.hdf +++ b/plugins/partition/language.hdf @@ -55,6 +55,23 @@ EnvironmentWarning { Link.Text = Initialize partition Link.Rel = partition } + + + + MissingProgramSfdisk { + Title = Missing program + Text = The program 'sfdisk' is not installed. Please ask the administrator of the CryptoBox to configure it properly. + } + + MissingProgramMkfs { + Title = Missing program + Text = The program 'mkfs' is not installed. Please ask the administrator of the CryptoBox to configure it properly. + } + + MissingProgramE2label { + Title = Missing program + Text = The program 'e2label' is not installed. Please ask the administrator of the CryptoBox to configure it properly. + } } diff --git a/plugins/partition/partition.py b/plugins/partition/partition.py index d563455..4b8c93c 100644 --- a/plugins/partition/partition.py +++ b/plugins/partition/partition.py @@ -99,12 +99,20 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin): def get_warnings(self): + warnings = [] ## this check is done _after_ "reset_dataset" -> if there is ## a config partition, then it was loaded before if self.cbox.prefs.requires_partition() \ and not self.cbox.prefs.get_active_partition(): - return (50, "Plugins.%s.ReadOnlyConfig" % self.get_name()) - return None + warnings.append((50, "Plugins.%s.ReadOnlyConfig" % self.get_name())) + ## check required programs + if not os.path.isfile(self.root_action.SFDISK_BIN): + warnings.append((53, "Plugins.%s.MissingProgramSfdisk" % self.get_name())) + if not os.path.isfile(self.root_action.MKFS_BIN): + warnings.append((56, "Plugins.%s.MissingProgramMkfs" % self.get_name())) + if not os.path.isfile(self.root_action.LABEL_BIN): + warnings.append((40, "Plugins.%s.MissingProgramE2label" % self.get_name())) + return warnings def __prepare_dataset(self): diff --git a/plugins/plugin-interface.txt b/plugins/plugin-interface.txt index 2bbabf9..819d1ac 100644 --- a/plugins/plugin-interface.txt +++ b/plugins/plugin-interface.txt @@ -41,6 +41,9 @@ Python code interface: - the class variable "plugin_capabilities" must be an array of strings (supported: "system" and "volume") - method "is_useful(self, device)": defaults to "True" - overwrite it, if there could be circumstances, which could make the plugin useless - e.g. "automount" is not useful for encrypted containers + - method "get_warnings(self)": return a tuple of (Priority, WarningName) or None if + no problems exist + - "WarningName" should be something like "Plugins.PLUGINNAME.NoSSL" - the class variable "plugin_visibility" may contain one or more of the following items: menu/preferences/volume. This should fit to the 'plugin_capabilities' variable. An empty list is interpreted as an invisible plugin. @@ -48,6 +51,8 @@ Python code interface: for this plugin - the class variable "rank" is an integer in the range of 0..100 - it determines the order of plugins in listings (lower value -> higher priority) + - the class variable "root_action" is None or the module as sourced out of "root_actions.py" + in the directory of the plugin - this allows to access constant settings in this file - volume plugins contain the attribute "device" (you may trust this value - a volume plugin will never get called with an invalid device) - the python module which contains the plugin's class should also contain a class called diff --git a/src/cryptobox/core/settings.py b/src/cryptobox/core/settings.py index b534605..05bed7e 100644 --- a/src/cryptobox/core/settings.py +++ b/src/cryptobox/core/settings.py @@ -58,10 +58,12 @@ class CryptoBoxSettings: self.plugin_conf = self.__get_plugin_config() self.user_db = self.__get_user_db() self.misc_files = [] - self.__read_misc_files() + self.reload_misc_files() - def __read_misc_files(self): + def reload_misc_files(self): + """Call this method after creating or removing a 'misc' configuration file + """ self.misc_files = self.__get_misc_files() @@ -92,6 +94,14 @@ class CryptoBoxSettings: return status + def get_misc_config_filename(self, name): + """Return an absolute filename for a given filename 'name' + + 'name' should not contain slashes (no directory part!) + """ + return os.path.join(self.prefs["Locations"]["SettingsDir"], "misc", name) + + def requires_partition(self): return bool(self.prefs["Main"]["UseConfigPartition"]) @@ -127,8 +137,10 @@ class CryptoBoxSettings: return False conf_partitions = self.get_available_partitions() if not conf_partitions: - self.log.error("no configuration partition found - you have to create " + self.log.warn("no configuration partition found - you have to create " + "it first") + #TODO: mount tmpfs in settings directory + self.log.info("Ramdisk (tmpfs) mounted as config partition ...") return False partition = conf_partitions[0] proc = subprocess.Popen( diff --git a/src/cryptobox/plugins/base.py b/src/cryptobox/plugins/base.py index 8059a26..7f1797d 100644 --- a/src/cryptobox/plugins/base.py +++ b/src/cryptobox/plugins/base.py @@ -28,6 +28,7 @@ __revision__ = "$Id" import os import cherrypy +import imp class CryptoBoxPlugin: @@ -72,6 +73,12 @@ class CryptoBoxPlugin: self.defaults = {} self.cbox.log.debug("Plugin '%s': configuration " % self.get_name() + \ "settings imported from global config file: %s" % str(self.defaults)) + ## load a possibly existing "root_action.py" scripts as self.root_action + if os.path.isfile(os.path.join(self.plugin_dir, "root_action.py")): + self.root_action = imp.load_source("root_action", + os.path.join(self.plugin_dir, "root_action.py")) + else: + self.root_action = None def do_action(self, **args): @@ -121,7 +128,7 @@ class CryptoBoxPlugin: - 20..39 heavy security risk OR broken recommended features - 00..19 possible mild security risk OR broken/missing optional features """ - return None + return [] @cherrypy.expose diff --git a/src/cryptobox/web/sites.py b/src/cryptobox/web/sites.py index 1627e8e..4440ce7 100644 --- a/src/cryptobox/web/sites.py +++ b/src/cryptobox/web/sites.py @@ -421,9 +421,7 @@ class WebInterfaceSites: """ warnings = [] for pl in self.__plugin_manager.get_plugins(): - warnings.append(pl.get_warnings()) - ## remove empty warnings - warnings = [ e for e in warnings if e ] + warnings.extend(pl.get_warnings()) warnings.sort(reverse=True) for (index, (warn_prio, warn_text)) in enumerate(warnings): self.__dataset["Data.EnvironmentWarning.%d" % index] = warn_text