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}
+
+
+
+
+
+
+
+
+
+