#!/usr/bin/python2.5 # -*- coding: utf-8 -*- """ $Id$ A web interface for managing htpasswd files. Copyright 2010 Lars Kruse This module 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 3 of the License, or (at your option) any later version. This module 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 This module. If not, see . """ import sys import os # add the current path to the python path - for "htpasswd" sys.path.insert(0, os.path.dirname(__file__)) # necessary for etch sys.path.insert(1, '/usr/share/pyshared') import htpasswd import bobo import genshi.template import ConfigParser import re BASE_DIR = os.path.dirname(__file__) CONFIG_FILE_LOCATIONS = [os.path.join(BASE_DIR, "htman.conf"), '/etc/htman/htman.conf'] TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates') REGEX = { "username": r"[a-zA-Z0-9_\-\.]+$", "password": r"[a-zA-Z0-9_\-\.%\$_\;,<>=+\[\]\{\}\(\)#\'\"\/\&\*@]+$", "mapping": r"[a-zA-Z0-9_\-\.]+$", } # use the environment variable HTMAN_CONFIG as the config file location if the # variable is defined if "HTMAN_CONFIG" in os.environ: CONFIG_FILE_LOCATIONS = [os.environ["HTMAN_CONFIG"]] @bobo.query('') def redirect_frontpage(): return bobo.redirect(web_defaults["base_url"] + '/') @bobo.query('/') def show_frontpage(): values = web_defaults.copy() return render("frontpage.html", **values) @bobo.query('/admin') def show_files(): values = web_defaults.copy() # The template expects a list of tuples: (mapping name, admin-url). # We assume, that the admin-url is just below the main admin URL. Thus # there is no need for generating a specific URL. all_zones = get_mapping().keys() all_zones.sort() values["mapping"] = [(zone_name, "%s%s/%s" % (web_defaults["base_url"], "admin/manage", zone_name)) for zone_name in all_zones] return render("list_mappings.html", **values) @bobo.query('/manage') def show_htpasswd(htname=None): if (htname is None) or (not htname in get_mapping()) or (not re.match(REGEX["mapping"], htname)): return bobo.redirect(web_defaults["base_url"]) else: # do a redirect: this allows the webserver to check the permissions for this URL return bobo.redirect("%smanage/%s" % (web_defaults["base_url"], str(htname))) # the alternative "/admin" URL allows to define super-user htaccess rules via # Apache's "Location" directive @bobo.query('/admin/manage/:htname') @bobo.query('/manage/:htname') def manage_htpasswd(htname=None, action=None, username=None, password=None): values = web_defaults.copy() values["error"] = None values["success"] = None if (htname is None) or (not htname in get_mapping()) or (not re.match(REGEX["mapping"], htname)): return bobo.redirect(web_defaults["base_url"]) values["htname"] = htname htpasswd_file = get_htpasswd_filename(htname) do_create_file = not os.path.isfile(htpasswd_file) try: htdb = htpasswd.HtpasswdFile(htpasswd_file, create=do_create_file) except IOError: values["error"] = "Failed to read htpasswd file" htdb = None else: if action is None: # just show the current state pass elif not username: values["error"] = "The username may not be empty!" elif not re.match(REGEX["username"], username): values["error"] = "The username contains invalid characters!" elif action == "del": if username in htdb.get_usernames(): htdb.delete(username) htdb.save() values["success"] = "User removed" else: values["error"] = "The user does not exist!" elif not password: values["error"] = "The password may not be empty!" elif not re.match(REGEX["password"], password): values["error"] = "The password contains invalid characters!" elif action == "update": if username in htdb.get_usernames(): htdb.update(username, password) htdb.save() values["success"] = "Password changed" else: values["error"] = "The user does not exist!" elif action == "new": if not username in htdb.get_usernames(): htdb.update(username, password) htdb.save() values["success"] = "User added" else: values["error"] = "The user exists already!" else: values["error"] = "Invalid action" if not htdb is None: values["usernames"] = htdb.get_usernames() else: values["usernames"] = [] # show the current htpasswd file return render("manage_users.html", **values) def get_config(): config = ConfigParser.SafeConfigParser() for filename in CONFIG_FILE_LOCATIONS: if os.path.isfile(filename): try: config.read(filename) except IOError: # ignore errors pass else: return config print >>sys.stderr, "Failed to load config file from %s" % str(CONFIG_FILE_LOCATIONS) sys.exit(1) def get_mapping(): try: mapping_file = config.get("Locations", "mapping") except ConfigParser.NoOptionError: print >>sys.stderr, "The location of the mapping file is not " \ "defined in the config file: [Locations] -> mapping" sys.exit(2) if not os.path.isfile(mapping_file): print >>sys.stderr, "The mapping file does not exist: %s" \ % str(mapping_file) sys.exit(2) # read the mapping file try: input_data = open(mapping_file, "r").readlines() except IOError: print >>sys.stderr, "Failed to open mapping file: %s" % mapping_file sys.exit(4) mapping = {} for line in input_data: if line.startswith("#"): continue if "=" in line: name, location = line.split("=", 1) name = name.strip() location = location.strip() # ignore invalid mapping names if re.match(REGEX["mapping"], name): mapping[name] = location return mapping def get_htpasswd_filename(htname): basename = mapping[htname] if basename and (basename != os.path.abspath(basename)): # relative filename in mapping file # let's try the htpasswd_directory setting try: htpasswd_directory = config.get("Locations", "htpasswd_directory") return os.path.join(htpasswd_directory, basename) except ConfigParser.NoOptionError: return os.path.abspath(basename) else: return basename def get_templates_dir(config): try: templates_dir = config.get("Locations", "templates") except ConfigParser.NoOptionError: # use the default templates_dir = TEMPLATES_DIR if not os.path.isdir(templates_dir): print >>sys.stderr, "Templates directory not found: %s" % templates_dir sys.exit(3) return os.path.abspath(templates_dir) def render(filename, input_data=None, **values): stream = loader.load(filename).generate(**values) if not input_data is None: stream |= genshi.filters.HTMLFormFiller(data=input_data) return stream.render("html", doctype="html") # *** Initialization *** config = get_config() # defaults to the standard TEMPLATES_DIR mapping = get_mapping() templates_dir = get_templates_dir(config) web_defaults = dict(config.items("Web")) loader = genshi.template.TemplateLoader([templates_dir], auto_reload=False) # this line allows to use mod_wsgi # see: http://groups.google.com/group/bobo-web/msg/2ba55fc381658cd1 # see: http://blog.dscpl.com.au/2009/08/using-bobo-on-top-of-modwsgi.html if __name__.startswith("_mod_wsgi_"): application = bobo.Application(bobo_resources=__name__)