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:
parent
c4d4ea399d
commit
ad3de60dd1
14 changed files with 292 additions and 34 deletions
|
@ -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" "$@"
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue