# # Copyright 2006 sense.lab e.V. # # This file is part of the CryptoBox. # # The CryptoBox is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # The CryptoBox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with the CryptoBox; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ this module handles all http requests and renders a website """ __revision__ = "$Id$" import cryptobox.core.main import cryptobox.web.dataset import cryptobox.plugins.manage import cryptobox.core.exceptions import re import cherrypy import os import sys try: import neo_cgi, neo_util, neo_cs except ImportError: _ERRMSG = "Could not import clearsilver module. \ Try 'apt-get install python-clearsilver'." sys.stderr.write(_ERRMSG) raise ImportError, _ERRMSG GETTEXT_DOMAIN = 'cryptobox-server' class PluginIconHandler: """deliver the icons of available plugins via cherrypy the state (enabled/disabled) and the require-auth setting is ignored to avoid repetitive reloading""" def __init__(self, plugins): for plugin in plugins.get_plugins(): if not plugin: continue plname = plugin.get_name() ## expose the get_icon function of this plugin setattr(self, plname, plugin.get_icon) class PluginDownloadHandler: """deliver downloadable files of available plugins via cherrypy the state (enabled/disabled) and the require-auth setting is ignored """ def __init__(self, plugins): for plugin in plugins.get_plugins(): if not plugin: continue plname = plugin.get_name() ## expose the get_icon function of this plugin setattr(self, plname, plugin.download) class WebInterfaceSites: """handle all http requests and render pages this includes: - filtering common arguments - calling feature actions - translating content all available features are dynamically exposed """ ## this template is used under strange circumstances defaultTemplate = "empty" def __init__(self, conf_file=None): ## we should only use variables preceded by "__" to avoid name conflicts ## when loading features self.cbox = cryptobox.core.main.CryptoBox(conf_file) self.__cached_language_data = None self.__dataset = None ## load the plugin manager - we will not try to detect new plugins on ## the fly ... self.__plugin_manager = cryptobox.plugins.manage.PluginManager( self.cbox, self.cbox.prefs["Locations"]["PluginDir"], self) self.__reset_dataset() ## store the original http error handler self._cp_on_http_error = self.new_http_error_handler ## set initial language order self.lang_order = self.cbox.prefs["WebSettings"]["Languages"][:] ## publish plugin icons self.icons = PluginIconHandler(self.__plugin_manager) self.icons.exposed = True ## publish plugin downloads self.downloads = PluginDownloadHandler(self.__plugin_manager) self.downloads.exposed = True ## announce that the server started up self.setup() def setup(self): """Prepare the webinterface. """ self.cbox.setup() for plugin in self.__plugin_manager.get_plugins(): if plugin: plugin.handle_event("bootup") 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.handle_event("shutdown") self.cbox.cleanup() def __reset_dataset(self): """this method has to be called at the beginning of every "site" action important: only at the beginning of an action (to not loose information) important: for _every_ "site" action (cherrypy is stateful) also take care for the plugins, as they also contain datasets """ self.__load_plugins() self.__dataset = cryptobox.web.dataset.WebInterfaceDataset( self.cbox, self.cbox.prefs, self.__plugin_manager) ## check, if a configuration partition has become available self.cbox.prefs.prepare_partition() def __load_plugins(self): """reinitialize the list of available plugins this includes the following: - reload all plugins and check their state (disabled or not) - reinitilize the datasets of all plugins """ #TODO: in the long-term we should create a separate object that only # contains the plugin handlers - this avoids some hassle of namespace # conflicts - this object will be the cherrypy.server.root # finish this for v0.4 for plugin in self.__plugin_manager.get_plugins(): if not plugin: continue plname = plugin.get_name() ## remove the old plugin handler and attach a new one try: ## check if there are name conflicts: e.g. a local variable has ## the same name as a plugin to be loaded -> skip these plugins ## if we would not check this here, nasty effects could occour prev_obj = getattr(self, plname) if not callable(prev_obj) or not prev_obj.exposed: ## name conflict - see below raise NameError ## remove the plugin handler delattr(self, plname) except AttributeError: ## "self" does not contain the given "plname" element ## this is ok, as we are just cleaning up pass except NameError: ## the attribute "exposed" of the element self."plname" does ## not exist - it seems, that we have a name conflict self.cbox.log.error("Skipping feature (%s) as its" % plname + " name conflicts with a local variable - see" + " module cryptobox.web.sites") ## skip this plugin continue ## the old attribute was cleaned up - we can reinitialize it now if plugin.is_enabled(): self.cbox.log.info("Plugin '%s' loaded" % plname) ## expose all features as URLs setattr(self, plname, self.return_plugin_action(plugin)) getattr(self, plname).exposed = True #TODO: check, if the stream_response feature really works #for now the "stream_response" feature seems to be broken #setattr(getattr(self, plname), "stream_respones", True) else: self.cbox.log.info("Plugin '%s' is disabled" % plname) ## nothing else has to be done ## sub pages requiring authentication may not be defined above def __request_auth(self=None): """ this is a function decorator to check authentication """ def check_credentials(site): """ see description of _inner_wrapper - please simplify this! """ def _inner_wrapper(self, *args, **kargs): """this function was necessary while trying around with the function decorator - if someone can implement the decorator with less effort, then any suggestions are welcome! """ import base64 ## define a "non-allowed" function user, password = None, None try: ## ignore the "Basic " (first six letters) part resp = cherrypy.request.headers["Authorization"][6:] (user, password) = base64.b64decode(resp).split(":", 1) except KeyError: ## no "authorization" header was sent pass except TypeError: ## invalid base64 string pass except AttributeError: ## no cherrypy request header defined pass auth_dict = self.cbox.prefs.user_db["admins"] if user in auth_dict.keys(): if self.cbox.prefs.user_db.get_digest(password) == auth_dict[user]: ## ok: return the choosen page self.cbox.log.info("access granted for: %s" % user) return site(self, *args, **kargs) else: self.cbox.log.info( "wrong password supplied for: %s" % user) else: self.cbox.log.info("unknown user: %s" % str(user)) ## wrong credentials: return "access denied" cherrypy.response.headers["WWW-Authenticate"] = \ '''Basic realm="CryptoBox"''' cherrypy.response.status = 401 return self.__render("access_denied") return _inner_wrapper return check_credentials ###################################################################### ## put real sites down here and don't forget to expose them at the end @cherrypy.expose def index(self, weblang="", help="0", device=None): """the default page on startup - we show the list of available disks """ self.__reset_dataset() self.__set_web_lang(weblang) self.__check_environment() ## do not forget the language! param_dict = {"weblang":weblang} ## render "disks" plugin by default return self.return_plugin_action( self.__plugin_manager.get_plugin("disks"))(**param_dict) def new_http_error_handler(self, error_code, message): """handle http errors gracefully 404 - not found errors: ignored if url is below /cryptobox-misc/ other 404 errors: send the error code and return a nice informative page 500 - runtime errors: return "ok" exit code and show a polite excuse others: are there any other possible http errors? """ import traceback ## we ignore uninteresting not-found errors if (error_code == 404) and \ (cherrypy.request.path.startswith("/cryptobox-misc/") or \ cherrypy.request.path in ['/robots.txt','/favicon.ico']): cherrypy.response.status = error_code return ## an invalid action was requested if error_code == 404: ## we send a not-found error (with the usual interface) cherrypy.response.status = error_code self.__dataset["Data.Warning"] = "InvalidAction" cherrypy.response.body = self.__render("empty") return ## are there still bugs in the code? if error_code == 500: ## we fix the error code (200 is "OK") cherrypy.response.status = 200 self.cbox.log.error( "HTTP-ERROR[500] - runtime error: %s" % str(message)) ## add a traceback and exception information to the lo for log_line in traceback.format_exception(*sys.exc_info()): self.cbox.log.error("\t%s" % log_line) self.__dataset["Data.Warning"] = "RuntimeError" cherrypy.response.body = self.__render("empty") return ## unknown error type cherrypy.response.status = error_code self.cbox.log.warn("HTTP-ERROR[%d] - an unknown error occoured: %s" \ % (error_code, message)) cherrypy.response.body = self.__render("empty") def return_plugin_action(self, plugin): """ returns a function that is suitable for handling a cherrypy page request """ def handler(self, weblang="", device=None, help="0", redirect=None, message_keep=None, **args): """this function handles a cherrypy page request """ plugin.reset() self.__reset_dataset() self.__check_environment() self.__set_web_lang(weblang) ## we always read the "device" setting - otherwise volume-plugin ## links would not work easily ## (see "volume_props" linking to "volume_format_fs") ## it will get ignored for non-volume plugins plugin.device = None if device and self.__set_device(device): plugin.device = device ## check the device argument of volume plugins if "volume" in plugin.plugin_capabilities: ## initialize the dataset of the selected device if necessary if plugin.device: self.__dataset.set_current_disk_state(plugin.device) else: ## invalid (or missing) device setting return self.__render(self.defaultTemplate) ## check if there is a "redirect" setting - this will override ## the return value of the do_action function ## (e.g. useful for umount-before-format) override_next_template = None if redirect: override_next_template = { "plugin": redirect } if "volume" in plugin.plugin_capabilities: override_next_template["values"] = {"device":plugin.device} ## check for information to be kept after the last call if message_keep: for (key, value) in message_keep["dataset"].items(): self.__dataset[key] = value ## check if the device is busy if plugin.device and self.cbox.get_container(plugin.device).is_busy(): return self.__render("volume_busy") ## call the plugin handler next_template = plugin.do_action(**args) ## for 'volume' plugins: reread the dataset of the current disk ## additionally: set the default template for plugins if "volume" in plugin.plugin_capabilities: ## maybe the state of the current volume was changed? self.__dataset.set_current_disk_state(plugin.device) if not next_template: next_template = { "plugin":"volume_mount", "values":{"device":plugin.device}} else: ## some non-volume plugins change the internal state of other ## plugins - e.g.: plugin_manager ## if we do not call __load_plugins now, then it is possible ## to call a plugin directly after disabling it (only once) self.__load_plugins() self.__dataset.set_plugin_data() ## default page for non-volume plugins is the disk selection if not next_template: next_template = { "plugin":"disks", "values":{} } #TODO: there is a lot of piece-by-piece updating around here # for v0.4 we should just call __reset_dataset - but this would # require to store the currently changed dataset values (e.g.i # weblang) somewhere else to not override it ## some non-volume plugins may change the state of containers ## the mount plugin may change the number of active disks - for the logo self.__dataset.set_containers_state() ## was a redirect requested? if override_next_template: next_template = override_next_template ## if another plugins was choosen for 'next_template', then do it! if isinstance(next_template, dict) \ and "plugin" in next_template.keys() \ and "values" in next_template.keys() \ and self.__plugin_manager.get_plugin(next_template["plugin"]): value_dict = dict(next_template["values"]) ## force the current weblang attribute - otherwise it gets lost value_dict["weblang"] = self.lang_order[0] ## check for warnings/success messages, that should be kept if "Data.Success" in plugin.hdf.keys() \ or "Data.Warning" in plugin.hdf.keys(): value_dict["message_keep"] = {"plugin":plugin, "dataset":{}} for keep_key in ("Data.Warning", "Data.Success"): if keep_key in plugin.hdf.keys(): self.cbox.log.info("keeping message: %s" % \ plugin.hdf[keep_key]) value_dict["message_keep"]["dataset"][keep_key] = \ plugin.hdf[keep_key] new_plugin = self.__plugin_manager.get_plugin(next_template["plugin"]) return self.return_plugin_action(new_plugin)(**value_dict) ## save the currently active plugin name self.__dataset["Data.ActivePlugin"] = plugin.get_name() return self.__render(next_template, plugin) ## apply authentication? if plugin.is_auth_required(): return lambda **args: self.__request_auth()(handler)(self, **args) else: return lambda **args: handler(self, **args) @cherrypy.expose def test(self, weblang="", help="0", device=None): """test authentication - this function may be safely removed """ self.__reset_dataset() self.__set_web_lang(weblang) self.__check_environment() result = "