#!/usr/bin/env python2 """ 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__)) from flask_bootstrap import Bootstrap from flask import * import ConfigParser import re import htpasswd 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_\-\.]+$", } app = Flask(__name__) app.config['BOOTSTRAP_SERVE_LOCAL'] = False bootstrap = Bootstrap(app) # 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"]] #@app.route('') #def redirect_frontpage(): # return bobo.redirect(web_defaults["base_url"]) @app.route('/') def show_frontpage(): values = web_defaults.copy() # return render("frontpage.html", **values) return render_template("frontpage.html") @app.route('/password') 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." return render_template("password_change.html", input_data=input_data, **values) @app.route('/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_template("list_mappings.html", **values) def is_zone_valid(zone): return zone and (zone in get_mapping()) and re.match(REGEX["mapping"], zone) @app.route('/manage') def show_htpasswd(zone=None): if not is_zone_valid(zone): 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(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 # the alternative "/admin" URL allows to define super-user htaccess rules via # Apache's "Location" directive @app.route('/admin/manage/:zone') @app.route('/manage/:zone') def manage_htpasswd(zone=None, action=None, username=None, password=None): values = web_defaults.copy() values["error"] = None values["success"] = None if not is_zone_valid(zone): return bobo.redirect(web_defaults["base_url"]) values["zone"] = zone htdb = get_htpasswd(zone, auto_create=True) if not htdb: values["error"] = "Failed to read htpasswd file" values["usernames"] = [] 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 return render_template("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(zone): basename = mapping[zone] 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")) #if not web_defaults["base_url"].endswith("/"): # web_defaults["base_url"] += "/" #loader = genshi.template.TemplateLoader([templates_dir], auto_reload=False) if __name__ == '__main__': app.run()