htman: switch from bobo to bottle
bobo does not support python3
This commit is contained in:
parent
50c8d6f75c
commit
953cb2f935
2 changed files with 78 additions and 61 deletions
119
htman/htman.py
119
htman/htman.py
|
@ -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,7 +270,7 @@ 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
|
||||||
|
@ -250,7 +279,7 @@ def get_htpasswd_filename(zone):
|
||||||
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__)
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue