htman/htpasswd.py

114 lines
3.9 KiB
Python

# -*- coding: utf-8 -*-
"""
A python module for managing htpasswd files.
The code is based on
http://trac.edgewall.org/export/9825/trunk/contrib/htpasswd.py
Original author: Eli Carter
Copyright 2010 Lars Kruse <devel@sumpfralle.de>
Copyright before 2010 Eli Carter
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/>.
"""
import crypt
import shutil
import tempfile
import random
import os
def _get_random_salt():
"""Returns a string of 2 random letters"""
letters = 'abcdefghijklmnopqrstuvwxyz' \
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \
'0123456789/.'
return random.choice(letters) + random.choice(letters)
class HtpasswdFile:
"""A class for manipulating htpasswd files."""
def __init__(self, filename, create=False):
self.entries = {}
self.filename = filename
if not create:
self.load()
def load(self):
"""Read the htpasswd file into memory.
This function may raise an IOError exception.
"""
self.entries = {}
for line in open(self.filename, 'r').readlines():
if ":" in line:
username, pwhash = line.split(':', 1)
# remove spaces
username = username.strip()
# remove newline characters and spaces
pwhash = pwhash.strip()
self.entries[username] = pwhash
def save(self):
"""Write the htpasswd file to disk
The file is written in a safe manner: first a temporary file with the
resulting content is created in the directory of the original file.
Afterwards it is moved to the original filename.
This function may raise an IOError exception.
"""
# create a temporary file besides the original file
temp_file, temp_filename = tempfile.mkstemp(
dir=os.path.dirname(self.filename), text=False)
try:
if os.path.isfile(self.filename):
# copy the original file mode (mod, timestamps)
shutil.copystat(self.filename, temp_filename)
sorted_names = sorted(self.entries.keys())
for name in sorted_names:
line = "%s:%s%s" % (name, self.entries[name], os.linesep)
os.write(temp_file, line.encode())
os.close(temp_file)
except IOError:
try:
os.remove(temp_filename)
except IOError:
# ignore errors during failure handling
pass
else:
# move the temporary file to the original filename
os.rename(temp_filename, self.filename)
def update(self, username, password):
"""Replace the entry for the given user, or add it if new."""
pwhash = crypt.crypt(password, _get_random_salt())
self.entries[username] = pwhash
def delete(self, username):
"""Remove the entry for the given user."""
if username in self.entries:
self.entries.pop(username)
def verify(self, username, password):
"""Check if the given password matches the hash."""
if username in self.entries:
crypthash = self.entries[username]
return crypt.crypt(password, crypthash) == crypthash
else:
return False
def get_usernames(self):
names = sorted(self.entries.keys())
return names