2021-02-09 21:05:41 +01:00
|
|
|
#!/usr/bin/python2.5
|
|
|
|
# -*- coding: utf-8 -*-
|
2010-06-05 03:22:57 +02:00
|
|
|
"""
|
2021-02-09 21:05:41 +01:00
|
|
|
$Id$
|
2010-06-05 03:22:57 +02:00
|
|
|
|
|
|
|
A web interface for managing htpasswd files.
|
|
|
|
|
|
|
|
Copyright 2010 Lars Kruse <devel@sumpfralle.de>
|
|
|
|
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
|
|
|
2010-06-09 01:57:23 +02:00
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
# add the current path to the python path - for "htpasswd"
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
2021-02-09 21:05:41 +01:00
|
|
|
# necessary for etch
|
|
|
|
sys.path.insert(1, '/usr/share/pyshared')
|
|
|
|
import htpasswd
|
|
|
|
import bobo
|
|
|
|
import genshi.template
|
|
|
|
import genshi.filters
|
2010-06-05 03:22:57 +02:00
|
|
|
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"]]
|
|
|
|
|
2012-09-20 03:51:08 +02:00
|
|
|
|
2021-02-09 21:05:41 +01:00
|
|
|
@bobo.query('')
|
|
|
|
def redirect_frontpage():
|
|
|
|
return bobo.redirect(web_defaults["base_url"])
|
2012-09-20 03:51:08 +02:00
|
|
|
|
2012-07-17 11:19:34 +02:00
|
|
|
|
2021-02-09 21:05:41 +01:00
|
|
|
@bobo.query('/')
|
2010-06-05 03:22:57 +02:00
|
|
|
def show_frontpage():
|
|
|
|
values = web_defaults.copy()
|
2021-02-09 21:05:41 +01:00
|
|
|
return render("frontpage.html", **values)
|
2010-06-05 03:22:57 +02:00
|
|
|
|
2012-09-20 03:51:08 +02:00
|
|
|
|
2021-02-09 21:05:41 +01:00
|
|
|
@bobo.query('/password')
|
2012-09-20 03:51:08 +02:00
|
|
|
def change_password(zone=None, username=None, old_password=None,
|
|
|
|
new_password=None, new_password2=None):
|
|
|
|
if zone:
|
|
|
|
zone = zone.strip()
|
|
|
|
if username:
|
|
|
|
username = username.strip()
|
|
|
|
if old_password:
|
|
|
|
old_password = old_password.strip()
|
|
|
|
if new_password:
|
|
|
|
new_password = new_password.strip()
|
|
|
|
if new_password2:
|
|
|
|
new_password2 = new_password2.strip()
|
|
|
|
values = web_defaults.copy()
|
|
|
|
values["error"] = None
|
|
|
|
values["success"] = None
|
|
|
|
input_data = {"zone": zone, "username": username}
|
|
|
|
verified = False
|
|
|
|
if is_zone_valid(zone):
|
|
|
|
htdb = get_htpasswd(zone)
|
|
|
|
if htdb and username and (username in htdb.get_usernames()) and \
|
|
|
|
htdb.verify(username, old_password):
|
|
|
|
verified = True
|
|
|
|
if old_password is None:
|
|
|
|
# first visit of this page: no action, no errors
|
|
|
|
pass
|
|
|
|
elif new_password != new_password2:
|
|
|
|
values["error"] = "New passwords do not match."
|
|
|
|
elif not new_password:
|
|
|
|
values["error"] = "No new password given."
|
|
|
|
elif verified:
|
|
|
|
htdb.update(username, new_password)
|
|
|
|
htdb.save()
|
|
|
|
values["success"] = "Password changed successfully."
|
|
|
|
else:
|
|
|
|
values["error"] = "Authentication error: zone, username or password is invalid."
|
2021-02-09 21:05:41 +01:00
|
|
|
return render("password_change.html", input_data=input_data, **values)
|
2012-09-20 03:51:08 +02:00
|
|
|
|
|
|
|
|
2021-02-09 21:05:41 +01:00
|
|
|
@bobo.query('/admin')
|
2010-06-05 03:22:57 +02:00
|
|
|
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.
|
2011-03-28 21:48:56 +02:00
|
|
|
all_zones = get_mapping().keys()
|
2010-06-09 01:57:23 +02:00
|
|
|
all_zones.sort()
|
|
|
|
values["mapping"] = [(zone_name, "%s%s/%s" % (web_defaults["base_url"], "admin/manage", zone_name)) for zone_name in all_zones]
|
2021-02-09 21:05:41 +01:00
|
|
|
return render("list_mappings.html", **values)
|
2010-06-05 03:22:57 +02:00
|
|
|
|
2012-09-20 03:51:08 +02:00
|
|
|
|
|
|
|
def is_zone_valid(zone):
|
|
|
|
return zone and (zone in get_mapping()) and re.match(REGEX["mapping"], zone)
|
|
|
|
|
|
|
|
|
2021-02-09 21:05:41 +01:00
|
|
|
@bobo.query('/manage')
|
2012-09-20 03:51:08 +02:00
|
|
|
def show_htpasswd(zone=None):
|
|
|
|
if not is_zone_valid(zone):
|
2010-06-05 03:22:57 +02:00
|
|
|
return bobo.redirect(web_defaults["base_url"])
|
|
|
|
else:
|
|
|
|
# do a redirect: this allows the webserver to check the permissions for this URL
|
2012-09-20 03:51:08 +02:00
|
|
|
return bobo.redirect("%smanage/%s" % (web_defaults["base_url"], str(zone)))
|
|
|
|
|
|
|
|
|
|
|
|
def get_htpasswd(zone, auto_create=False):
|
|
|
|
htpasswd_file = get_htpasswd_filename(zone)
|
|
|
|
do_create_file = auto_create and not os.path.isfile(htpasswd_file)
|
|
|
|
try:
|
|
|
|
return htpasswd.HtpasswdFile(htpasswd_file, create=do_create_file)
|
|
|
|
except IOError:
|
|
|
|
return None
|
|
|
|
|
2010-06-05 03:22:57 +02:00
|
|
|
|
|
|
|
# the alternative "/admin" URL allows to define super-user htaccess rules via
|
|
|
|
# Apache's "Location" directive
|
2021-02-09 21:05:41 +01:00
|
|
|
@bobo.query('/admin/manage/:zone')
|
|
|
|
@bobo.query('/manage/:zone')
|
2012-09-20 03:51:08 +02:00
|
|
|
def manage_htpasswd(zone=None, action=None, username=None, password=None):
|
2010-06-05 03:22:57 +02:00
|
|
|
values = web_defaults.copy()
|
|
|
|
values["error"] = None
|
|
|
|
values["success"] = None
|
2012-09-20 03:51:08 +02:00
|
|
|
if not is_zone_valid(zone):
|
2010-06-05 03:22:57 +02:00
|
|
|
return bobo.redirect(web_defaults["base_url"])
|
2012-09-20 03:51:08 +02:00
|
|
|
values["zone"] = zone
|
|
|
|
htdb = get_htpasswd(zone, auto_create=True)
|
|
|
|
if not htdb:
|
2010-06-05 03:22:57 +02:00
|
|
|
values["error"] = "Failed to read htpasswd file"
|
2012-09-20 03:51:08 +02:00
|
|
|
values["usernames"] = []
|
2010-06-05 03:22:57 +02:00
|
|
|
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"
|
|
|
|
values["usernames"] = htdb.get_usernames()
|
|
|
|
# show the current htpasswd file
|
2021-02-09 21:05:41 +01:00
|
|
|
return render("manage_users.html", **values)
|
2010-06-05 03:22:57 +02:00
|
|
|
|
2012-09-20 03:51:08 +02:00
|
|
|
|
2010-06-05 03:22:57 +02:00
|
|
|
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)
|
|
|
|
|
2012-09-20 03:51:08 +02:00
|
|
|
|
2011-03-28 21:48:56 +02:00
|
|
|
def get_mapping():
|
2010-06-05 03:22:57 +02:00
|
|
|
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
|
|
|
|
|
2012-09-20 03:51:08 +02:00
|
|
|
|
|
|
|
def get_htpasswd_filename(zone):
|
|
|
|
basename = mapping[zone]
|
2010-06-09 01:57:23 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2010-06-05 03:22:57 +02:00
|
|
|
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)
|
|
|
|
|
2012-09-20 03:51:08 +02:00
|
|
|
|
2010-06-05 03:22:57 +02:00
|
|
|
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
|
2011-03-28 21:48:56 +02:00
|
|
|
mapping = get_mapping()
|
2010-06-05 03:22:57 +02:00
|
|
|
templates_dir = get_templates_dir(config)
|
|
|
|
web_defaults = dict(config.items("Web"))
|
2021-02-09 21:05:41 +01:00
|
|
|
if not web_defaults["base_url"].endswith("/"):
|
|
|
|
web_defaults["base_url"] += "/"
|
|
|
|
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__)
|
2010-06-05 03:22:57 +02:00
|
|
|
|