htman: switch from bobo to bottle

bobo does not support python3
This commit is contained in:
lars 2021-02-09 21:12:49 +00:00
parent 4b517a736e
commit 8a185e878a
2 changed files with 78 additions and 61 deletions

121
htman.py
View file

@ -1,8 +1,5 @@
#!/usr/bin/python2.5 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" """
$Id$
A web interface for managing htpasswd files. A web interface for managing htpasswd files.
Copyright 2010 Lars Kruse <devel@sumpfralle.de> Copyright 2010 Lars Kruse <devel@sumpfralle.de>
@ -21,21 +18,23 @@ You should have received a copy of the GNU General Public License
along with This module. If not, see <http://www.gnu.org/licenses/>. along with This module. If not, see <http://www.gnu.org/licenses/>.
""" """
import sys import configparser
import re
import os import os
# add the current path to the python path - for "htpasswd" import sys
sys.path.insert(0, os.path.dirname(__file__))
# necessary for etch import bottle
sys.path.insert(1, '/usr/share/pyshared')
import htpasswd
import bobo
import genshi.template import genshi.template
import genshi.filters import genshi.filters
import ConfigParser
import re import htpasswd
BASE_DIR = os.path.dirname(__file__) BASE_DIR = os.path.dirname(__file__)
CONFIG_FILE_LOCATIONS = [os.path.join(BASE_DIR, "htman.conf"), '/etc/htman/htman.conf'] CONFIG_FILE_LOCATIONS = [
os.path.join(BASE_DIR, "htman.conf"),
'/etc/htman/htman.conf',
]
TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates') TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates')
REGEX = { REGEX = {
"username": r"[a-zA-Z0-9_\-\.]+$", "username": r"[a-zA-Z0-9_\-\.]+$",
@ -48,20 +47,28 @@ if "HTMAN_CONFIG" in os.environ:
CONFIG_FILE_LOCATIONS = [os.environ["HTMAN_CONFIG"]] CONFIG_FILE_LOCATIONS = [os.environ["HTMAN_CONFIG"]]
@bobo.query('') @bottle.route('')
def redirect_frontpage(): def redirect_frontpage():
return bobo.redirect(web_defaults["base_url"]) return bottle.redirect(web_defaults["base_url"])
@bobo.query('/') @bottle.route('/')
def show_frontpage(): def show_frontpage():
values = web_defaults.copy() values = web_defaults.copy()
return render("frontpage.html", **values) return render("frontpage.html", **values)
@bobo.query('/password') @bottle.route('/password')
def change_password(zone=None, username=None, old_password=None, def change_password_get(zone=None):
new_password=None, new_password2=None): return change_password(zone)
@bottle.route('/password', method='POST')
def change_password(zone=None):
username = bottle.request.forms.get('username')
old_password = bottle.request.forms.get('old_password')
new_password = bottle.request.forms.get('new_password')
new_password2 = bottle.request.forms.get('new_password2')
if zone: if zone:
zone = zone.strip() zone = zone.strip()
if username: if username:
@ -94,33 +101,43 @@ def change_password(zone=None, username=None, old_password=None,
htdb.save() htdb.save()
values["success"] = "Password changed successfully." values["success"] = "Password changed successfully."
else: else:
values["error"] = "Authentication error: zone, username or password is invalid." values["error"] = (
"Authentication error: zone, username or password is invalid.")
return render("password_change.html", input_data=input_data, **values) return render("password_change.html", input_data=input_data, **values)
@bobo.query('/admin') @bottle.route('/admin')
def show_files(): def show_files():
values = web_defaults.copy() values = web_defaults.copy()
# The template expects a list of tuples: (mapping name, admin-url). # 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 # We assume, that the admin-url is just below the main admin URL. Thus
# there is no need for generating a specific URL. # there is no need for generating a specific URL.
all_zones = get_mapping().keys() all_zones = sorted(get_mapping().keys())
all_zones.sort() values["mapping"] = [
values["mapping"] = [(zone_name, "%s%s/%s" % (web_defaults["base_url"], "admin/manage", zone_name)) for zone_name in all_zones] (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) return render("list_mappings.html", **values)
def is_zone_valid(zone): def is_zone_valid(zone):
return zone and (zone in get_mapping()) and re.match(REGEX["mapping"], zone) return (
zone
and (zone in get_mapping())
and re.match(REGEX["mapping"], zone)
)
@bobo.query('/manage') @bottle.route('/manage')
def show_htpasswd(zone=None): def show_htpasswd(zone=None):
if not is_zone_valid(zone): if not is_zone_valid(zone):
return bobo.redirect(web_defaults["base_url"]) return bottle.redirect(web_defaults["base_url"])
else: else:
# do a redirect: this allows the webserver to check the permissions for this URL # do a redirect: this allows the webserver to check the permissions
return bobo.redirect("%smanage/%s" % (web_defaults["base_url"], str(zone))) # for this URL
return bottle.redirect(
"%smanage/%s" % (web_defaults["base_url"], str(zone)))
def get_htpasswd(zone, auto_create=False): def get_htpasswd(zone, auto_create=False):
@ -132,16 +149,25 @@ def get_htpasswd(zone, auto_create=False):
return None return None
@bottle.route('/admin/manage/:zone')
@bottle.route('/manage/:zone')
def mange_htpasswd_get(zone=None):
return manage_htpasswd(zone)
# the alternative "/admin" URL allows to define super-user htaccess rules via # the alternative "/admin" URL allows to define super-user htaccess rules via
# Apache's "Location" directive # Apache's "Location" directive
@bobo.query('/admin/manage/:zone') @bottle.route('/admin/manage/:zone', method='POST')
@bobo.query('/manage/:zone') @bottle.route('/manage/:zone', method='POST')
def manage_htpasswd(zone=None, action=None, username=None, password=None): def manage_htpasswd(zone=None):
action = bottle.request.forms.get('action')
username = bottle.request.forms.get('username')
password = bottle.request.forms.get('password')
values = web_defaults.copy() values = web_defaults.copy()
values["error"] = None values["error"] = None
values["success"] = None values["success"] = None
if not is_zone_valid(zone): if not is_zone_valid(zone):
return bobo.redirect(web_defaults["base_url"]) return bottle.redirect(web_defaults["base_url"])
values["zone"] = zone values["zone"] = zone
htdb = get_htpasswd(zone, auto_create=True) htdb = get_htpasswd(zone, auto_create=True)
if not htdb: if not htdb:
@ -174,7 +200,7 @@ def manage_htpasswd(zone=None, action=None, username=None, password=None):
else: else:
values["error"] = "The user does not exist!" values["error"] = "The user does not exist!"
elif action == "new": elif action == "new":
if not username in htdb.get_usernames(): if username not in htdb.get_usernames():
htdb.update(username, password) htdb.update(username, password)
htdb.save() htdb.save()
values["success"] = "User added" values["success"] = "User added"
@ -188,7 +214,7 @@ def manage_htpasswd(zone=None, action=None, username=None, password=None):
def get_config(): def get_config():
config = ConfigParser.SafeConfigParser() config = configparser.ConfigParser()
for filename in CONFIG_FILE_LOCATIONS: for filename in CONFIG_FILE_LOCATIONS:
if os.path.isfile(filename): if os.path.isfile(filename):
try: try:
@ -198,14 +224,17 @@ def get_config():
pass pass
else: else:
return config return config
print >>sys.stderr, "Failed to load config file from %s" % str(CONFIG_FILE_LOCATIONS) print(
"Failed to load config file from %s" % str(CONFIG_FILE_LOCATIONS),
file=sys.stderr
)
sys.exit(1) sys.exit(1)
def get_mapping(): def get_mapping():
try: try:
mapping_file = config.get("Locations", "mapping") mapping_file = config.get("Locations", "mapping")
except ConfigParser.NoOptionError: except configparser.NoOptionError:
print >>sys.stderr, "The location of the mapping file is not " \ print >>sys.stderr, "The location of the mapping file is not " \
"defined in the config file: [Locations] -> mapping" "defined in the config file: [Locations] -> mapping"
sys.exit(2) sys.exit(2)
@ -241,16 +270,16 @@ def get_htpasswd_filename(zone):
try: try:
htpasswd_directory = config.get("Locations", "htpasswd_directory") htpasswd_directory = config.get("Locations", "htpasswd_directory")
return os.path.join(htpasswd_directory, basename) return os.path.join(htpasswd_directory, basename)
except ConfigParser.NoOptionError: except configparser.NoOptionError:
return os.path.abspath(basename) return os.path.abspath(basename)
else: else:
return basename return basename
def get_templates_dir(config): def get_templates_dir(config):
try: try:
templates_dir = config.get("Locations", "templates") templates_dir = config.get("Locations", "templates")
except ConfigParser.NoOptionError: except configparser.NoOptionError:
# use the default # use the default
templates_dir = TEMPLATES_DIR templates_dir = TEMPLATES_DIR
if not os.path.isdir(templates_dir): if not os.path.isdir(templates_dir):
@ -261,7 +290,7 @@ def get_templates_dir(config):
def render(filename, input_data=None, **values): def render(filename, input_data=None, **values):
stream = loader.load(filename).generate(**values) stream = loader.load(filename).generate(**values)
if not input_data is None: if input_data is not None:
stream |= genshi.filters.HTMLFormFiller(data=input_data) stream |= genshi.filters.HTMLFormFiller(data=input_data)
return stream.render("html", doctype="html") return stream.render("html", doctype="html")
@ -275,11 +304,3 @@ web_defaults = dict(config.items("Web"))
if not web_defaults["base_url"].endswith("/"): if not web_defaults["base_url"].endswith("/"):
web_defaults["base_url"] += "/" web_defaults["base_url"] += "/"
loader = genshi.template.TemplateLoader([templates_dir], auto_reload=False) 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__)

View file

@ -1,11 +1,9 @@
#!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
$Id$
A python module for managing htpasswd files. A python module for managing htpasswd files.
The code is based on http://trac.edgewall.org/export/9825/trunk/contrib/htpasswd.py The code is based on
http://trac.edgewall.org/export/9825/trunk/contrib/htpasswd.py
Original author: Eli Carter Original author: Eli Carter
Copyright 2010 Lars Kruse <devel@sumpfralle.de> Copyright 2010 Lars Kruse <devel@sumpfralle.de>
@ -72,15 +70,15 @@ class HtpasswdFile:
""" """
# create a temporary file besides the original file # create a temporary file besides the original file
temp_file, temp_filename = tempfile.mkstemp( temp_file, temp_filename = tempfile.mkstemp(
dir=os.path.dirname(self.filename), text=True) dir=os.path.dirname(self.filename), text=False)
try: try:
if os.path.isfile(self.filename): if os.path.isfile(self.filename):
# copy the original file mode (mod, timestamps) # copy the original file mode (mod, timestamps)
shutil.copystat(self.filename, temp_filename) shutil.copystat(self.filename, temp_filename)
sorted_names = self.entries.keys() sorted_names = sorted(self.entries.keys())
sorted_names.sort()
for name in sorted_names: for name in sorted_names:
os.write(temp_file, "%s:%s%s" % (name, self.entries[name], os.linesep)) line = "%s:%s%s" % (name, self.entries[name], os.linesep)
os.write(temp_file, line.encode())
os.close(temp_file) os.close(temp_file)
except IOError: except IOError:
try: try:
@ -111,7 +109,5 @@ class HtpasswdFile:
return False return False
def get_usernames(self): def get_usernames(self):
names = self.entries.keys() names = sorted(self.entries.keys())
names.sort()
return names return names