From 0cfc0ebb44ef5cc78ad06f0afad5297064cc9a5f Mon Sep 17 00:00:00 2001 From: lars Date: Sat, 5 Jun 2010 01:22:57 +0000 Subject: [PATCH] added web interface code added basic templates --- htman.py | 204 ++++++++++++++++++++++++++++++++++++ templates/frontpage.html | 18 ++++ templates/layout.html | 44 ++++++++ templates/manage_users.html | 60 +++++++++++ 4 files changed, 326 insertions(+) create mode 100644 htman.py create mode 100644 templates/frontpage.html create mode 100644 templates/layout.html create mode 100644 templates/manage_users.html diff --git a/htman.py b/htman.py new file mode 100644 index 0000000..efb89ad --- /dev/null +++ b/htman.py @@ -0,0 +1,204 @@ +#!/usr/bin/python +# -*- 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 htpasswd +import bobo +import genshi.template +import ConfigParser +import re +import sys +import os + +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('') +@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. + values["mapping"] = [(mapping, mapping) for mapping in mapping.keys()] + return render("list_mappings.html", **values) + +@bobo.query('/manage') +def show_htpasswd(htname=None): + if (htname is None) or (not htname in 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/: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 mapping) or (not re.match(REGEX["mapping"], htname)): + return bobo.redirect(web_defaults["base_url"]) + values["htname"] = htname + htpasswd_file = mapping[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(config): + 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_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(config) +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__ == "__main__": + application = bobo.Application(bobo_resources=__name__) + diff --git a/templates/frontpage.html b/templates/frontpage.html new file mode 100644 index 0000000..e319e06 --- /dev/null +++ b/templates/frontpage.html @@ -0,0 +1,18 @@ + + + + + + + +
+ + + +
+ + + diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..70aafcd --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,44 @@ + + + + + + + ${title} + + + + + + + + + + + +
${select('*|text()')}
+ + + +
+ + diff --git a/templates/manage_users.html b/templates/manage_users.html new file mode 100644 index 0000000..39530ba --- /dev/null +++ b/templates/manage_users.html @@ -0,0 +1,60 @@ + + + + + + + +

Manage user access for ${htname}

+ +
${error}
+ +

Add user

+
+ + +
+ + + + + +
+ + +
+ +

Change password

+
+ + +
+ + + + + +
+ +
+ +

Delete user

+
+ + + + + +
+
+ + + +