From 9ec9475015466fcca8106470a2d22fd153c097b0 Mon Sep 17 00:00:00 2001 From: lars Date: Tue, 19 Dec 2006 14:02:30 +0000 Subject: [PATCH] better permission handling of startup script minor bugfixes update of CryptoBoxWebserver manpage --- bin/CryptoBoxRootActions | 2 +- bin/CryptoBoxWebserver | 184 +++++++++++++++++++++++++------- debian/changelog | 4 +- debian/cryptobox-server.default | 1 - debian/cryptobox-server.init | 8 +- debian/cryptobox-server.postrm | 1 + intl/de/cryptobox-server.po | 55 +++++----- man/CryptoBoxWebserver.8 | 18 ++-- src/cryptobox/__init__.py | 2 +- src/cryptobox/core/container.py | 2 +- src/cryptobox/core/main.py | 5 +- src/cryptobox/core/settings.py | 3 + src/cryptobox/web/sites.py | 2 + 13 files changed, 200 insertions(+), 87 deletions(-) diff --git a/bin/CryptoBoxRootActions b/bin/CryptoBoxRootActions index c519729..6a342c3 100755 --- a/bin/CryptoBoxRootActions +++ b/bin/CryptoBoxRootActions @@ -50,7 +50,7 @@ allowedProgs = { ## this line is necessary to run unittests - otherwise these tests are too strict # TODO: check this before every release! -OVERRIDE_FILECHECK = True +OVERRIDE_FILECHECK = False DEV_TYPES = { "pipe":1, "char":2, "dir":4, "block":6, "file":8, "link":10, "socket":12} EVENT_MARKER = '_event_scripts_' diff --git a/bin/CryptoBoxWebserver b/bin/CryptoBoxWebserver index e676cee..28d297a 100755 --- a/bin/CryptoBoxWebserver +++ b/bin/CryptoBoxWebserver @@ -67,7 +67,7 @@ except: SERVER_ENVIRONMENT = "production" class CryptoBoxWebserver: - '''this class starts the cherryp webserver and serves the single sites''' + '''this class starts the cherrypy webserver and serves the single sites''' def __init__(self, opts): self.opts = opts @@ -77,20 +77,27 @@ class CryptoBoxWebserver: sys.exit(1) ## store the absolute path as we will chdir later (for daemons) self.conffile = os.path.realpath(opts.conffile) + ## drop privileges to run the cryptobox without root permissions + self.drop_privileges_temporarily() ## initialize site class try: cherrypy.root = cryptobox.web.sites.WebInterfaceSites(self.conffile) - except (CBConfigError,CBEnvironmentError), errMsg: + self.website = cherrypy.root + except (CBConfigError,CBEnvironmentError), err_msg: sys.stderr.write("Error: the CryptoBox is misconfigured - please fix it!\n") - sys.stderr.write("%s\n" % str(errMsg)) + sys.stderr.write("%s\n" % str(err_msg)) sys.exit(1) + ## restore privileges, as we need them to connect to a low socket (<1024) + self.restore_privileges() ## expose static content and set options + ## beware: cherrypy.config.update({ "global": { "server.socket_port" : int(opts.port), "server.socket_host" : opts.host, "server.log_to_screen" : not opts.background and opts.verbose, "server.log_tracebacks" : opts.verbose, + "server.log_request_headers": opts.verbose, "server.environment": SERVER_ENVIRONMENT, "server.log_file" : opts.logfile }, "/cryptobox-misc": { @@ -101,49 +108,102 @@ class CryptoBoxWebserver: "staticFilter.file": os.path.realpath(os.path.join(opts.datadir, 'favicon.ico'))} }) - self.define_exit_handlers(cherrypy.root) - def define_exit_handlers(self, cbw): - import atexit - import signal - ## define exit handler for normal termination (via sys.exit) - def exit_handler(): - cbw.cleanup() - try: - os.remove(self.opts.pidfile) - except OSError: - pass - atexit.register(exit_handler) - ## catch kill signal - def kill_signal_handler(signum, frame): - cbw.cbox.log.info("Kill signal handler called: %d" % signum) - sys.exit(1) - signal.signal(signal.SIGTERM, kill_signal_handler) + def get_user_info(self): + """Retrieve the uid, gid and additional groups of the given user + """ + import pwd, grp + user_entry = pwd.getpwuid(self.opts.user) + ## get the new uid and gid + pw_uid, pw_gid = user_entry[2], user_entry[3] + ## change the owner of the webserver log file + try: + os.chown(self.opts.logfile, pw_uid, pw_gid) + except OSError: + ## fail silently + pass + ## calculate additional groups of the given user + additional_groups = [ entry[2] + for entry in grp.getgrall() + if pw_uid in entry[3] ] + return (pw_uid, pw_gid, additional_groups) + + + def drop_privileges_temporarily(self): + """Temporarily drop privileges. + """ + if self.opts.user is None: + return + (pw_uid, pw_gid, additional_groups) = self.get_user_info() + try: + os.setegid(pw_gid) + os.seteuid(pw_uid) + except OSError, err_msg: + sys.stderr.write("Failed to drop privileges temporarily: %s\n" % err_msg) + + + def restore_privileges(self): + """Restore previously temporarily dropped privileges. + """ + if self.opts.user is None: + return + try: + os.setegid(os.getgid()) + os.seteuid(os.getuid()) + except OSError, err_msg: + sys.stderr.write("Failed to restore privileges: %s\n" % err_msg) + + + def drop_privileges_permanently(self): + """Drop all privileges of the current process and acquire the privileges of the + given user instead. + """ + if self.opts.user is None: + return + (pw_uid, pw_gid, additional_groups) = self.get_user_info() + try: + os.setgroups(additional_groups) + os.setregid(pw_gid, pw_gid) + os.setreuid(pw_uid, pw_uid) + except OSError, err_msg: + sys.stderr.write("Failed to drop privileges permanently: %s\n" % err_msg) + def start(self): - cherrypy.server.start() + try: + cherrypy.server.start(initOnly=True) + self.drop_privileges_permanently() + cherrypy.server.wait_for_http_ready() + except cherrypy._cperror.NotReady, err_msg: + sys.stderr.write("Failed to start CryptoBox: %s\n" % err_msg) + sys.exit(1) + except Exception, err_msg: + if err_msg == "(98, 'Address already in use')": + sys.stderr.write("Failed to start CryptoBox: %s\n" % err_msg) + sys.exit(1) + else: + raise - def fork_to_background(): ## this is just copy'n'pasted from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731 ## check the original for exhaustive comments try: pid = os.fork() - except OSError, errMsg: + except OSError, err_msg: sys.stderr.write("Error: failed to fork cryptobox daemon process!\n") - sys.stderr.write("%s\n" % errMsg) + sys.stderr.write("%s\n" % err_msg) sys.exit(1) if pid == 0: # the first child os.setsid() try: pid = os.fork() - except OSError, errMsg: + except OSError, err_msg: sys.stderr.write("Error: failed to fork second cryptobox daemon process!\n") - sys.stderr.write("%s\n" % errMsg) + sys.stderr.write("%s\n" % err_msg) sys.exit(1) if pid == 0: # the second child ## we do not change the directory - otherwise there seems to be a race condition with the python interpreter loading this script file @@ -163,16 +223,17 @@ def close_open_files(): ## use the following lines to close all open files (including the log file) # maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] # if (maxfd == resource.RLIM_INFINITY): - # maxfd = 1024 + # maxfd = 1024 maxfd = 2 for fd in range(0, maxfd): try: - os.close(fd) + ## close all except for stderr - we will redirect it later + if fd != 2: + os.close(fd) except OSError: # ERROR, fd wasn't open to begin with (ignored) pass os.open(os.devnull, os.O_RDWR) # standard input (0) os.dup2(0, 1) # standard output (1) - os.dup2(0, 2) # standard error (2) def write_pid_file(pid_file): @@ -183,14 +244,15 @@ def write_pid_file(pid_file): pidf = open(pid_file,"w") pidf.write(str(os.getpid())) pidf.close() - except (IOError, OSError), errMsg: + except (IOError, OSError), err_msg: sys.stderr.write( - "Warning: failed to write pid file (%s): %s\n" % (pid_file, errMsg)) + "Warning: failed to write pid file (%s): %s\n" % (pid_file, err_msg)) ## it is just a warning - no need to break def parseOptions(): import cryptobox + import pwd version = "%prog" + cryptobox.__version__ parser = OptionParser(version=version) parser.set_defaults(conffile="/etc/cryptobox-server/cryptobox.conf", @@ -200,7 +262,8 @@ def parseOptions(): logfile="/var/log/cryptobox-server/webserver.log", port="8080", host="", - verbose=True) + verbose=True, + user=None) parser.add_option("-c", "--config", dest="conffile", help="read configuration from FILE", metavar="FILE") parser.add_option("","--pidfile", dest="pidfile", @@ -217,6 +280,8 @@ def parseOptions(): help="write webserver log to FILE") parser.add_option("","--host", dest="host", metavar="HOST", help="attach to HOST") + parser.add_option("-u","--user", dest="user", metavar="USER", + help="change to USER after starting the webserver") (options, args) = parser.parse_args() ## we do not expect any remaining arguments if len(args) != 0: @@ -232,29 +297,66 @@ def parseOptions(): parser.error("invalid port number: %s" % str(options.port)) except ValueError: parser.error("invalid port specified (%s) - it must be a number" % (options.port)) + if options.user: + try: + try: + ## check for the user given as uid + uid = pwd.getpwuid(int(options.user))[2] + except ValueError: + ## check for the user given as name + uid = pwd.getpwnam(options.user)[2] + except KeyError: + ## invalid user specified + parser.error("invalid user specified (%s)" % options.user) + ## we will use the uid + options.user = uid return options + + if __name__ == "__main__": ## process arguments options = parseOptions() ## set umask to 022 (aka 755) - octal value os.umask(022) - ## run the webserver as a daemon process - if options.background: fork_to_background() - ## write pid file - write_pid_file(options.pidfile) ## initialize the webserver class (before forking to get some error messages) cbw = CryptoBoxWebserver(options) - ## close open files to allow background execution - if options.background: close_open_files() + ## fork to background before cbw.start() - otherwise we lose the socket + if options.background: + fork_to_background() ## start the webserver try: cbw.start() - except CBError, errMsg: + except CBError, err_msg: sys.stderr.write("Failed to start the CryptoBox webserver!\n") - sys.stderr.write("%s\n" % str(errMsg)) + sys.stderr.write("%s\n" % str(err_msg)) sys.stderr.write("Check the log file for details.\n") sys.exit(1) - sys.exit(0) + ## redirect stderr to the webserver's logfile + if options.background: + ## replace stdin and stdout by /dev/null + close_open_files() + ## replace stderr by the webserver logfile + os.close(2) + os.open(options.logfile, os.O_APPEND) + ## startup went fine - fork is done - now we may write the pid file + ## write pid file + write_pid_file(options.pidfile) + def exit_handler(signum, sigframe): + cbw.website.cbox.log.info("Shutting down ...") + cbw.website.cleanup() + try: + os.remove(options.pidfile) + except OSError: + pass + os._exit(0) + ## the signal handler gets called by a kill signal (usually in background mode) + import signal + signal.signal(signal.SIGTERM, exit_handler) + ## this exit handler gets called by KeyboardInterrupt and similar ones (foreground) + import atexit + atexit.register(exit_handler, None, None) + ## this will never exit - one of the above exit handlers will get triggered + cherrypy.server.block() diff --git a/debian/changelog b/debian/changelog index 7676fc8..244dbae 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -cryptobox (0.3.0.1-1) unstable; urgency=low +cryptobox (0.3.1-1) unstable; urgency=low * new upstream release - -- Lars Kruse Mon, 18 Dec 2006 09:07:32 +0100 + -- Lars Kruse Tue, 19 Dec 2006 12:34:57 +0100 cryptobox (0.3.0-1) unstable; urgency=low diff --git a/debian/cryptobox-server.default b/debian/cryptobox-server.default index c683485..4e40894 100644 --- a/debian/cryptobox-server.default +++ b/debian/cryptobox-server.default @@ -9,7 +9,6 @@ NO_START=1 RUNAS=cryptobox # listening port -# for now please use a port above 1024 PORT=8080 # some more server options (rarely necessary) diff --git a/debian/cryptobox-server.init b/debian/cryptobox-server.init index a881f90..61050c3 100644 --- a/debian/cryptobox-server.init +++ b/debian/cryptobox-server.init @@ -37,7 +37,7 @@ test -x "$DAEMON" || DAEMON=/usr/bin/CryptoBoxWebserver PYTHON_EXEC=/usr/bin/python PIDFILE=/var/run/cryptobox-server/webserver.pid DESC="CryptoBox Daemon (webinterface)" -OPTIONS="-B --pidfile=$PIDFILE --config=$CONF_FILE --logfile=$LOGFILE --host=$HOST --port=$PORT $SERVER_OPTS" +OPTIONS="-B --pidfile=$PIDFILE --config=$CONF_FILE --logfile=$LOGFILE --host=$HOST --port=$PORT --user=$RUNAS $SERVER_OPTS" # check if the package is installed test -e "$DAEMON" || exit 0 @@ -51,13 +51,13 @@ case "$1" in PIDDIR=$(dirname "$PIDFILE") if [ -d "$PIDDIR" ] then mkdir -p "$PIDDIR" + # necessary: the cryptobox server needs the permission to remove the pid file chown $RUNAS:root "$PIDDIR" chmod 755 "$PIDDIR" fi log_daemon_msg "Starting $DESC" if start-stop-daemon \ - --chuid $RUNAS: --quiet --start \ - --user $RUNAS --pidfile "$PIDFILE" \ + --quiet --start --user $RUNAS --pidfile "$PIDFILE" \ --startas "$PYTHON_EXEC" -- "$DAEMON" $OPTIONS then log_end_msg 0 else log_end_msg 1 @@ -82,7 +82,7 @@ case "$1" in ;; reload | force-reload | restart ) "$0" stop - sleep 1 + sleep 3 "$0" start ;; status ) diff --git a/debian/cryptobox-server.postrm b/debian/cryptobox-server.postrm index b52cf3a..01e4b4b 100644 --- a/debian/cryptobox-server.postrm +++ b/debian/cryptobox-server.postrm @@ -33,6 +33,7 @@ umount_all() remove_stuff() { + #TODO: remove old log files too (created by logrotate) test -e "$LOG_FILE" && rm "$LOG_FILE" test -e "$WEBLOG_FILE" && rm "$WEBLOG_FILE" test -e "$PID_DIR" && rm -r "$PID_DIR" diff --git a/intl/de/cryptobox-server.po b/intl/de/cryptobox-server.po index b1ee535..82fc703 100644 --- a/intl/de/cryptobox-server.po +++ b/intl/de/cryptobox-server.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: CryptoBox-Server 0.3\n" "Report-Msgid-Bugs-To: translate@cryptobox.org\n" -"POT-Creation-Date: 2006-11-28 05:03+0100\n" +"POT-Creation-Date: 2006-12-19 12:35+0100\n" "PO-Revision-Date: 2006-12-18 16:38+0100\n" "Last-Translator: Lars Kruse \n" "Language-Team: LANGUAGE \n" @@ -12,10 +12,6 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Pootle 0.10.1\n" -#: Name -msgid "English" -msgstr "Deutsch" - #: Title.Top msgid "The CryptoBox" msgstr "Die CryptoBox" @@ -57,20 +53,36 @@ msgid "The CryptoBox is a project of" msgstr "Die CryptoBox ist ein Projekt von" #: Text.ContainerName -msgid "Container's name" +msgid "Volume's name" msgstr "Name des Datenträgers" -#: Button.HelpForForm -msgid "Get help" -msgstr "Hilfe" +#: Button.EnableHelp +msgid "Enable help" +msgstr "Hilfe aktivieren" + +#: Button.DisableHelp +msgid "Disable help" +msgstr "Hilfe deaktivieren" + +#: AdviceMessage.VolumeIsBusy.Title +msgid "Disk is busy" +msgstr "Datenträger ist beschäftigt" + +#: AdviceMessage.VolumeIsBusy.Text +msgid "This disk is currently busy. Please wait for a moment." +msgstr "Der Datenträger ist noch beschäftigt. Warte bitte einen Moment." + +#: AdviceMessage.VolumeIsBusy.Link.Text +msgid "Show all disks" +msgstr "Zeige alle Datenträger" #: WarningMessage.AccessDenied.Title msgid "Invalid access credentials" msgstr "Ungültige Zugangsdaten" #: WarningMessage.AccessDenied.Text -msgid "Sorry - you are not allowed to do this!" -msgstr "Sorry - du darfst dies nicht tun!" +msgid "Sorry - you did not enter the right credentials! Maybe you should try the default setting: username=>'admin' / password=>'admin'." +msgstr "Tut mir Leid, du hast nicht die richtigen Zugangsdaten eingegeben! (Versuch es mit \"admin\" als Benutzername und als Passwort und ändere das Passwort bald.)" #: WarningMessage.EmptyPassword.Title msgid "Missing password" @@ -105,16 +117,16 @@ msgid "The device you have chosen is invalid!" msgstr "Der ausgewählte Datenträger kann nicht verwendet werden." #: WarningMessage.VolumeMayNotBeMounted.Title -msgid "The container is mounted" -msgstr "Der Datenträger ist geöffnet." +msgid "The volume is open" +msgstr "Der Datenträger ist geöffnet" #: WarningMessage.VolumeMayNotBeMounted.Text -msgid "This action is not available while the container is active. Please turn it off first." +msgid "This action is not available while the volume is active. Please close it first." msgstr "Diese Aktion kann nicht durchgeführt werden, solange der Datenträger geöffnet ist. Bitte schließe ihn zuvor." #: WarningMessage.VolumeMayNotBeMounted.Link.Text -msgid "Deactivate volume" -msgstr "Schließe den Datenträger" +msgid "Close volume" +msgstr "Schließe Datenträger" #: WarningMessage.InvalidAction.Title msgid "Invalid request" @@ -208,14 +220,3 @@ msgstr "Diese Aktion kann nicht durchgeführt werden, solange der Datenträger g msgid "Close volume" msgstr "Schließe Datenträger" -#~ msgid "volume_mount" -#~ msgstr "volume_mount" - -#~ msgid "logs" -#~ msgstr "logs" - -#~ msgid "partition" -#~ msgstr "partition" - -#~ msgid "https" -#~ msgstr "https" diff --git a/man/CryptoBoxWebserver.8 b/man/CryptoBoxWebserver.8 index 494ca3c..7e3f539 100644 --- a/man/CryptoBoxWebserver.8 +++ b/man/CryptoBoxWebserver.8 @@ -14,26 +14,30 @@ simple access to your data. The following options control the behaviour of the CryptoBoxWebserver: .TP \fB\-c\fR, \fB\-\-config\fR=\fBFILE\fR -Uses the named configuration file. +Use the specified configuration file. .TP \fB\-p\fR, \fB\-\-port\fR=\fBPORT\fR -Specifiy a port to listen to. +Specify a port to listen to. The default port is 8080. .TP \fB\-\-host\fR=\fBHOST\fR -Specifiy the interface to listen to by providing a resolvable name or an ip. The server +Specify the interface to listen to by providing a resolvable name or an ip. The server listens to all interfaces by default. .TP \fB\-B\fR -Run the webserver in the background. Otherwise the terminal will stay attached to the -process. +Run the webserver in the background. By default the process will stay attached to the +terminal. +.TP +\fB\-u\fR, \fB\-\-user\fR=\fBUSER\fR +Run with the permissions of the given user after connecting to the port. You may use a +uid or a name. .TP \fB\-q\fR Quiet output - only errors will get reported. .TP -\fB\-\-pidfile\fR=\fFILE\fR +\fB\-\-pidfile\fR=\fBFILE\fR Specify a pid file for the webserver. .TP -\fB\-\-datadir\fR=\fDIRECTORY\fR +\fB\-\-datadir\fR=\fBDIRECTORY\fR Specify the location of the data directory of the webserver. The default location is \fI/usr/share/cryptobox/www-data\fR. .TP diff --git a/src/cryptobox/__init__.py b/src/cryptobox/__init__.py index 3409b5a..4b2f01d 100644 --- a/src/cryptobox/__init__.py +++ b/src/cryptobox/__init__.py @@ -10,5 +10,5 @@ __all__ = ['core', 'web', 'plugins', 'tests'] __revision__ = "$Id$" -__version__ = "0.3.0.1" +__version__ = "0.3.1" diff --git a/src/cryptobox/core/container.py b/src/cryptobox/core/container.py index 1c4a1c4..af3f544 100644 --- a/src/cryptobox/core/container.py +++ b/src/cryptobox/core/container.py @@ -574,7 +574,7 @@ class CryptoBoxContainer: def __umount_plain(self): "umount a plaintext partition" if not self.is_mounted(): - self.cbox.log.info("trying to umount while volume (%s) is mounted" % \ + self.cbox.log.info("trying to umount while volume (%s) is not mounted" % \ self.get_device()) return self.cbox.send_event_notification("preumount", self.__get_event_args()) diff --git a/src/cryptobox/core/main.py b/src/cryptobox/core/main.py index 2482b88..5d99c29 100644 --- a/src/cryptobox/core/main.py +++ b/src/cryptobox/core/main.py @@ -66,12 +66,13 @@ class CryptoBox: self.log.info("Umounting all volumes ...") self.reread_container_list() for cont in self.get_container_list(): - cont.umount() + if cont.is_mounted(): + cont.umount() ## save all settings self.log.info("Storing local settings ...") ## problems with storing are logged automatically self.prefs.write() - if self.prefs.get_active_partition: + if self.prefs.get_active_partition(): self.prefs.umount_partition() ## shutdown logging as the last step try: diff --git a/src/cryptobox/core/settings.py b/src/cryptobox/core/settings.py index 70f4e6d..18b47e9 100644 --- a/src/cryptobox/core/settings.py +++ b/src/cryptobox/core/settings.py @@ -601,6 +601,9 @@ class MiscConfigFile: def save(self): """Save a configuration file to disk. """ + ## overriding of ro-files is not necessary (e.g. samba-include.conf) + if os.path.exists(self.filename) and not os.access(self.filename, os.W_OK): + return True save_dir = os.path.dirname(self.filename) ## create the directory, if necessary if not os.path.isdir(save_dir): diff --git a/src/cryptobox/web/sites.py b/src/cryptobox/web/sites.py index ff6dd15..03cda54 100644 --- a/src/cryptobox/web/sites.py +++ b/src/cryptobox/web/sites.py @@ -98,8 +98,10 @@ class WebInterfaceSites: def cleanup(self): """Shutdown the webinterface safely. """ + self.cbox.log.info("Shutting down webinterface ...") for plugin in self.__plugin_manager.get_plugins(): if plugin: + self.cbox.log.info("Cleaning up plugin '%s' ..." % plugin.get_name()) plugin.cleanup() self.cbox.cleanup()