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
This commit is contained in:
lars 2007-01-24 02:51:17 +00:00
parent c4d4ea399d
commit ad3de60dd1
14 changed files with 292 additions and 34 deletions

View file

@ -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" "$@"

View file

@ -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.
"""

View file

@ -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.
}
}

View file

@ -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)

View file

@ -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.
}
}

View file

@ -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)

View file

@ -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.
}
}

View file

@ -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:

View file

@ -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.
}
}

View file

@ -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):

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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