113 lines
3.9 KiB
Python
113 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
|