Edgewall Software

source: trunk/contrib/htpasswd.py

Last change on this file was 17759, checked in by Jun Omae, 8 months ago

1.7.1dev: merge [17757:17758] from 1.6-stable (fix for #13629)

[skip ci]

  • Property svn:eol-style set to native
  • Property svn:executable set to *
File size: 6.0 KB
Line 
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2008-2023 Edgewall Software
5# Copyright (C) 2008 Eli Carter
6# All rights reserved.
7#
8# This software is licensed as described in the file COPYING, which
9# you should have received as part of this distribution. The terms
10# are also available at https://trac.edgewall.org/wiki/TracLicense.
11#
12# This software consists of voluntary contributions made by many
13# individuals. For the exact contribution history, see the revision
14# history and logs, available at https://trac.edgewall.org/.
15
16import argparse
17import getpass
18import sys
19
20try:
21 import passlib
22except ImportError:
23 passlib = None
24 try:
25 import crypt
26 except ImportError:
27 crypt = None
28else:
29 crypt = None
30
31from trac.util.compat import wait_for_file_mtime_change
32from trac.util.text import printerr
33
34
35if passlib:
36 from passlib.context import CryptContext
37 _crypt_schemes = {
38 'sha256': 'sha256_crypt',
39 'sha512': 'sha512_crypt',
40 'md5': 'apr_md5_crypt',
41 'des': 'des_crypt',
42 }
43 from passlib.hash import bcrypt
44 try:
45 bcrypt.get_backend()
46 except passlib.exc.MissingBackendError:
47 pass
48 else:
49 _crypt_schemes['bcrypt'] = 'bcrypt'
50 _crypt_context = CryptContext(schemes=sorted(_crypt_schemes.values()))
51 _hash_methods = sorted(_crypt_schemes)
52 def hash_password(word, method):
53 scheme = _crypt_schemes[method]
54 if hasattr(_crypt_context, 'hash'): # passlib 1.7+
55 hash_ = _crypt_context.hash
56 else:
57 hash_ = _crypt_context.encrypt
58 return hash_(word, scheme=scheme)
59elif crypt:
60 _crypt_methods = {
61 'sha256': crypt.METHOD_SHA256,
62 'sha512': crypt.METHOD_SHA512,
63 'md5': None, # use md5crypt
64 'des': crypt.METHOD_CRYPT,
65 }
66 if hasattr(crypt, 'METHOD_BLOWFISH'):
67 _crypt_methods['bcrypt'] = crypt.METHOD_BLOWFISH
68 _hash_methods = sorted(_crypt_methods)
69 from trac.util import salt, md5crypt
70 def hash_password(word, method):
71 if method == 'md5':
72 return md5crypt(word, salt(), '$apr1$')
73 else:
74 return crypt.crypt(word, crypt.mksalt(_crypt_methods[method]))
75else:
76 printerr("The crypt module is not found. Install the passlib package "
77 "from PyPI.", newline=True)
78 sys.exit(1)
79
80
81def ask_pass():
82 pass1 = getpass.getpass('New password: ')
83 pass2 = getpass.getpass('Re-type new password: ')
84 if pass1 != pass2:
85 printerr("htpasswd: password verification error")
86 sys.exit(1)
87 return pass1
88
89
90class HtpasswdFile(object):
91 """A class for manipulating htpasswd files."""
92
93 def __init__(self, filename, create=False):
94 self.entries = []
95 self.filename = filename
96 if not create:
97 self.load()
98
99 def load(self):
100 """Read the htpasswd file into memory."""
101 self.entries = []
102 with open(self.filename, 'r', encoding='utf-8') as f:
103 for line in f:
104 username, pwhash = line.split(':')
105 entry = [username, pwhash.rstrip()]
106 self.entries.append(entry)
107
108 def save(self):
109 """Write the htpasswd file to disk"""
110 wait_for_file_mtime_change(self.filename)
111 with open(self.filename, 'w', encoding='utf-8') as f:
112 f.writelines("%s:%s\n" % (entry[0], entry[1])
113 for entry in self.entries)
114
115 def update(self, username, password, method):
116 """Replace the entry for the given user, or add it if new."""
117 pwhash = hash_password(password, method)
118 matching_entries = [entry for entry in self.entries
119 if entry[0] == username]
120 if matching_entries:
121 matching_entries[0][1] = pwhash
122 else:
123 self.entries.append([username, pwhash])
124
125 def delete(self, username):
126 """Remove the entry for the given user."""
127 self.entries = [entry for entry in self.entries
128 if entry[0] != username]
129
130
131def main():
132 """
133 %(prog)s [-c] passwordfile username
134 %(prog)s -b[c] passwordfile username password
135 %(prog)s -D passwordfile username\
136 """
137
138 parser = argparse.ArgumentParser(usage=main.__doc__)
139 parser.add_argument('-b', action='store_true', dest='batch',
140 help="batch mode; password is passed on the command "
141 "line IN THE CLEAR")
142 parser_group = parser.add_mutually_exclusive_group()
143 parser_group.add_argument('-c', action='store_true', dest='create',
144 help="create a new htpasswd file, overwriting "
145 "any existing file")
146 parser_group.add_argument('-D', action='store_true', dest='delete_user',
147 help="remove the given user from the password "
148 "file")
149 parser.add_argument('-t', dest='method', choices=_hash_methods,
150 default='md5', help='hash method for passwords '
151 '(default: %(default)s)')
152 parser.add_argument('passwordfile', help=argparse.SUPPRESS)
153 parser.add_argument('username', help=argparse.SUPPRESS)
154 parser.add_argument('password', nargs='?', help=argparse.SUPPRESS)
155
156 args = parser.parse_args()
157 password = args.password
158 if args.delete_user:
159 if password is not None:
160 parser.error("too many arguments")
161 else:
162 if args.batch and password is None:
163 parser.error("too few arguments")
164 elif not args.batch and password is not None:
165 parser.error("too many arguments")
166
167 try:
168 passwdfile = HtpasswdFile(args.passwordfile, create=args.create)
169 except EnvironmentError:
170 printerr("File not found.")
171 sys.exit(1)
172 else:
173 if args.delete_user:
174 passwdfile.delete(args.username)
175 else:
176 if password is None:
177 password = ask_pass()
178 passwdfile.update(args.username, password, args.method)
179 passwdfile.save()
180
181
182if __name__ == '__main__':
183 main()
Note: See TracBrowser for help on using the repository browser.