# -*- 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 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 . """ 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