htauth 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/HISTORY.md +14 -0
- data/Manifest.txt +4 -28
- data/exe/htdigest-ruby +14 -0
- data/exe/htpasswd-ruby +14 -0
- data/htauth.gemspec +33 -0
- data/lib/htauth/algorithm.rb +30 -29
- data/lib/htauth/argon2.rb +45 -36
- data/lib/htauth/bcrypt.rb +12 -11
- data/lib/htauth/cli/digest.rb +42 -46
- data/lib/htauth/cli/passwd.rb +126 -115
- data/lib/htauth/cli.rb +5 -3
- data/lib/htauth/console.rb +9 -6
- data/lib/htauth/crypt.rb +11 -9
- data/lib/htauth/descendant_tracker.rb +11 -9
- data/lib/htauth/digest_entry.rb +22 -20
- data/lib/htauth/digest_file.rb +25 -18
- data/lib/htauth/entry.rb +3 -1
- data/lib/htauth/error.rb +6 -5
- data/lib/htauth/file.rb +35 -39
- data/lib/htauth/md5.rb +35 -34
- data/lib/htauth/passwd_entry.rb +26 -24
- data/lib/htauth/passwd_file.rb +26 -21
- data/lib/htauth/plaintext.rb +7 -5
- data/lib/htauth/sha1.rb +9 -7
- data/lib/htauth/version.rb +3 -1
- data/lib/htauth.rb +29 -28
- metadata +15 -133
- data/Rakefile +0 -29
- data/bin/htdigest-ruby +0 -12
- data/bin/htpasswd-ruby +0 -12
- data/spec/algorithm_spec.rb +0 -7
- data/spec/argon2_spec.rb +0 -28
- data/spec/bcrypt_spec.rb +0 -32
- data/spec/cli/digest_spec.rb +0 -149
- data/spec/cli/passwd_spec.rb +0 -346
- data/spec/crypt_spec.rb +0 -11
- data/spec/digest_entry_spec.rb +0 -59
- data/spec/digest_file_spec.rb +0 -64
- data/spec/md5_spec.rb +0 -11
- data/spec/passwd_entry_spec.rb +0 -172
- data/spec/passwd_file_spec.rb +0 -84
- data/spec/plaintext_spec.rb +0 -11
- data/spec/sha1_spec.rb +0 -10
- data/spec/spec_helper.rb +0 -25
- data/spec/test.add.digest +0 -3
- data/spec/test.add.passwd +0 -3
- data/spec/test.delete.digest +0 -1
- data/spec/test.delete.passwd +0 -1
- data/spec/test.original.digest +0 -2
- data/spec/test.original.passwd +0 -2
- data/spec/test.update.digest +0 -2
- data/spec/test.update.passwd +0 -2
- data/tasks/default.rake +0 -250
- data/tasks/this.rb +0 -208
- /data/{LICENSE → LICENSE.txt} +0 -0
data/lib/htauth/cli/passwd.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
|
|
3
|
+
require "htauth/cli"
|
|
4
|
+
|
|
5
|
+
require "ostruct"
|
|
6
|
+
require "optparse"
|
|
5
7
|
|
|
6
8
|
module HTAuth
|
|
7
9
|
module CLI
|
|
8
10
|
# Internal: Implemenation of the commandline htpasswd-ruby
|
|
9
11
|
class Passwd
|
|
10
|
-
|
|
11
12
|
MAX_PASSWD_LENGTH = 255
|
|
12
13
|
|
|
13
14
|
attr_accessor :passwd_file
|
|
@@ -19,14 +20,14 @@ module HTAuth
|
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def options
|
|
22
|
-
if @options.nil?
|
|
23
|
+
if @options.nil?
|
|
23
24
|
@options = ::OpenStruct.new
|
|
24
25
|
@options.batch_mode = false
|
|
25
26
|
@options.file_mode = File::ALTER
|
|
26
27
|
@options.passwdfile = nil
|
|
27
28
|
@options.algorithm = Algorithm::EXISTING
|
|
28
29
|
@options.algorithm_args = {}
|
|
29
|
-
@options.read_stdin_once= false
|
|
30
|
+
@options.read_stdin_once = false
|
|
30
31
|
@options.send_to_stdout = false
|
|
31
32
|
@options.show_version = false
|
|
32
33
|
@options.show_help = false
|
|
@@ -38,96 +39,94 @@ module HTAuth
|
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def option_parser
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#{op.program_name} -b[acmBdpsD] [--verify] [-C cost] passwordfile username password
|
|
42
|
+
@option_parser ||= OptionParser.new(nil, 16) do |op|
|
|
43
|
+
op.banner = <<~BANNER
|
|
44
|
+
Usage:
|
|
45
|
+
#{op.program_name} [-acimBdpsD] [--verify] [-C cost] passwordfile username
|
|
46
|
+
#{op.program_name} -b[acmBdpsD] [--verify] [-C cost] passwordfile username password
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
#{op.program_name} -n[imBdps] [-C cost] username
|
|
49
|
+
#{op.program_name} -nb[mBdps] [-C cost] username password
|
|
50
|
+
BANNER
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
op.separator ""
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
op.on("--argon2", "Force argon2 encryption of the password.") do |_a|
|
|
55
|
+
options.algorithm = Algorithm::ARGON2
|
|
56
|
+
end
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
op.on("-b", "--batch", "Batch mode, get the password from the command line, rather than prompt.") do |b|
|
|
59
|
+
options.batch_mode = b
|
|
60
|
+
end
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
op.on("-B", "--bcrypt", "Force bcrypt encryption of the password.") do |_b|
|
|
63
|
+
options.algorithm = Algorithm::BCRYPT
|
|
64
|
+
end
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
cost = c.to_i
|
|
73
|
-
if (4..31).include?(cost)
|
|
74
|
-
options.algorithm_args = { :cost => cost }
|
|
75
|
-
else
|
|
76
|
-
raise ::OptionParser::ParseError, "the bcrypt cost must be an integer from 4 to 31, `#{c}` is invalid"
|
|
77
|
-
end
|
|
66
|
+
op.on("-CCOST", "--cost COST", "Set the computing time used for the bcrypt algorithm",
|
|
67
|
+
"(higher is more secure but slower, default: 5, valid: 4 to 31).") do |c|
|
|
68
|
+
unless /\A\d+\z/.match?(c)
|
|
69
|
+
raise ::OptionParser::ParseError, "the bcrypt cost must be an integer from 4 to 31, `#{c}` is invalid"
|
|
78
70
|
end
|
|
79
71
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
cost = c.to_i
|
|
73
|
+
unless (4..31).cover?(cost)
|
|
74
|
+
raise ::OptionParser::ParseError, "the bcrypt cost must be an integer from 4 to 31, `#{c}` is invalid"
|
|
83
75
|
end
|
|
84
76
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
end
|
|
77
|
+
options.algorithm_args = { cost: cost }
|
|
78
|
+
end
|
|
88
79
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
op.on("-c", "--create", "Create a new file; this overwrites an existing file.") do |_c|
|
|
81
|
+
options.file_mode = HTAuth::File::CREATE
|
|
82
|
+
options.operation << :add_or_update
|
|
83
|
+
end
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
op.on("-d", "--crypt", "Force CRYPT encryption of the password.") do |_c|
|
|
86
|
+
options.algorithm = Algorithm::CRYPT
|
|
87
|
+
end
|
|
96
88
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
op.on("-D", "--delete", "Delete the specified user.") do |_d|
|
|
90
|
+
options.operation << :delete
|
|
91
|
+
end
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
op.on("-h", "--help", "Display this help.") do |h|
|
|
94
|
+
options.show_help = h
|
|
95
|
+
end
|
|
104
96
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
options.operation << :stdout
|
|
109
|
-
end
|
|
97
|
+
op.on("-i", "--stdin", "Read the passwod from stdin without verivication (for script usage).") do |_i|
|
|
98
|
+
options.read_stdin_once = true
|
|
99
|
+
end
|
|
110
100
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
op.on("-m", "--md5", "Force MD5 encryption of the password (default).") do |_m|
|
|
102
|
+
options.algorithm = Algorithm::MD5
|
|
103
|
+
end
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
105
|
+
op.on("-n", "--stdout", "Do not update the file; Display the results on stdout instead.") do |_n|
|
|
106
|
+
options.send_to_stdout = true
|
|
107
|
+
options.passwdfile = HTAuth::File::STDOUT_FLAG
|
|
108
|
+
options.operation << :stdout
|
|
109
|
+
end
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
111
|
+
op.on("-p", "--plaintext", "Do not encrypt the password (plaintext).") do |_p|
|
|
112
|
+
options.algorithm = Algorithm::PLAINTEXT
|
|
113
|
+
end
|
|
122
114
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
op.on("-s", "--sha1", "Force SHA encryption of the password.") do |_s|
|
|
116
|
+
options.algorithm = Algorithm::SHA1
|
|
117
|
+
end
|
|
126
118
|
|
|
127
|
-
|
|
119
|
+
op.on("-v", "--version", "Show version info.") do |v|
|
|
120
|
+
options.show_version = v
|
|
121
|
+
end
|
|
128
122
|
|
|
129
|
-
|
|
123
|
+
op.on("--verify", "Verify password for the specified user.") do |_v|
|
|
124
|
+
options.operation << :verify
|
|
130
125
|
end
|
|
126
|
+
|
|
127
|
+
op.separator ""
|
|
128
|
+
|
|
129
|
+
op.separator "The SHA algorihtm does not use a salt and is less secure than the MD5 algorithm."
|
|
131
130
|
end
|
|
132
131
|
@option_parser
|
|
133
132
|
end
|
|
@@ -143,35 +142,48 @@ Usage:
|
|
|
143
142
|
end
|
|
144
143
|
|
|
145
144
|
def parse_options(argv)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
raise ::OptionParser::ParseError,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
raise ::OptionParser::ParseError,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
options.password = argv.shift if options.batch_mode
|
|
163
|
-
|
|
164
|
-
rescue ::OptionParser::ParseError => pe
|
|
165
|
-
$stderr.puts "ERROR: #{option_parser.program_name} - #{pe}"
|
|
166
|
-
show_help
|
|
167
|
-
exit 1
|
|
145
|
+
option_parser.parse!(argv)
|
|
146
|
+
show_version if options.show_version
|
|
147
|
+
show_help if options.show_help
|
|
148
|
+
|
|
149
|
+
if options.operation.size > 1
|
|
150
|
+
raise ::OptionParser::ParseError,
|
|
151
|
+
"only one of --create, --stdout, --verify, --delete may be specified"
|
|
152
|
+
end
|
|
153
|
+
if options.send_to_stdout && (options.file_mode == File::CREATE)
|
|
154
|
+
raise ::OptionParser::ParseError,
|
|
155
|
+
"Unable to send to stdout AND create a new file"
|
|
156
|
+
end
|
|
157
|
+
raise ::OptionParser::ParseError, "a username is needed" if options.send_to_stdout && argv.empty?
|
|
158
|
+
if options.send_to_stdout && options.batch_mode && (argv.size < 2)
|
|
159
|
+
raise ::OptionParser::ParseError,
|
|
160
|
+
"a username and password are needed"
|
|
168
161
|
end
|
|
162
|
+
if !options.send_to_stdout && options.batch_mode && (argv.size < 3)
|
|
163
|
+
raise ::OptionParser::ParseError,
|
|
164
|
+
"a passwordfile, username and password are needed "
|
|
165
|
+
end
|
|
166
|
+
raise ::OptionParser::ParseError, "a passwordfile and username are needed" if argv.size < 2
|
|
167
|
+
if options.batch_mode && options.read_stdin_once
|
|
168
|
+
raise ::OptionParser::ParseError,
|
|
169
|
+
"options -i and -b are mutually exclusive"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
options.operation = options.operation.shift || :add_or_update
|
|
173
|
+
options.passwdfile = argv.shift unless options.send_to_stdout
|
|
174
|
+
options.username = argv.shift
|
|
175
|
+
options.password = argv.shift if options.batch_mode
|
|
176
|
+
rescue ::OptionParser::ParseError => e
|
|
177
|
+
warn "ERROR: #{option_parser.program_name} - #{e}"
|
|
178
|
+
show_help
|
|
179
|
+
exit 1
|
|
169
180
|
end
|
|
170
181
|
|
|
171
|
-
def fetch_password(width=20)
|
|
182
|
+
def fetch_password(width = 20)
|
|
172
183
|
return options.password if options.batch_mode
|
|
184
|
+
|
|
173
185
|
console = Console.new
|
|
174
|
-
if options.read_stdin_once
|
|
186
|
+
if options.read_stdin_once
|
|
175
187
|
pw_in = console.read_answer
|
|
176
188
|
return pw_in
|
|
177
189
|
end
|
|
@@ -188,10 +200,10 @@ Usage:
|
|
|
188
200
|
raise PasswordError, "They don't match, sorry." unless pw_in == pw_validate
|
|
189
201
|
end
|
|
190
202
|
|
|
191
|
-
|
|
203
|
+
pw_in
|
|
192
204
|
end
|
|
193
205
|
|
|
194
|
-
def run(argv,
|
|
206
|
+
def run(argv, _env = ENV)
|
|
195
207
|
begin
|
|
196
208
|
parse_options(argv)
|
|
197
209
|
console = Console.new
|
|
@@ -201,16 +213,15 @@ Usage:
|
|
|
201
213
|
passwd_file.delete(options.username)
|
|
202
214
|
passwd_file.save!
|
|
203
215
|
when :verify
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
raise HTAuth::Error, "Password verification for user #{options.username} failed."
|
|
210
|
-
end
|
|
211
|
-
else
|
|
212
|
-
raise HTAuth::Error, "User #{options.username} not found"
|
|
216
|
+
raise HTAuth::Error, "User #{options.username} not found" unless passwd_file.has_entry?(options.username)
|
|
217
|
+
|
|
218
|
+
pw_in = fetch_password
|
|
219
|
+
unless passwd_file.authenticated?(options.username, pw_in)
|
|
220
|
+
raise HTAuth::Error, "Password verification for user #{options.username} failed."
|
|
213
221
|
end
|
|
222
|
+
|
|
223
|
+
warn "Password for user #{options.username} correct."
|
|
224
|
+
|
|
214
225
|
when :add_or_update
|
|
215
226
|
options.password = fetch_password
|
|
216
227
|
action = passwd_file.has_entry?(options.username) ? "Changing" : "Adding"
|
|
@@ -222,16 +233,16 @@ Usage:
|
|
|
222
233
|
passwd_file.add_or_update(options.username, options.password, options.algorithm, options.algorithm_args)
|
|
223
234
|
passwd_file.save!
|
|
224
235
|
end
|
|
225
|
-
rescue HTAuth::FileAccessError =>
|
|
236
|
+
rescue HTAuth::FileAccessError => e
|
|
226
237
|
msg = "Password file failure (#{options.passwdfile}) "
|
|
227
|
-
|
|
238
|
+
warn "#{msg}: #{e.message}"
|
|
228
239
|
exit 1
|
|
229
|
-
rescue HTAuth::Error =>
|
|
230
|
-
|
|
240
|
+
rescue HTAuth::Error => e
|
|
241
|
+
warn e.message
|
|
231
242
|
exit 1
|
|
232
|
-
rescue SignalException =>
|
|
243
|
+
rescue SignalException => e
|
|
233
244
|
$stderr.puts
|
|
234
|
-
|
|
245
|
+
warn "Interrupted #{e}"
|
|
235
246
|
exit 1
|
|
236
247
|
end
|
|
237
248
|
exit 0
|
data/lib/htauth/cli.rb
CHANGED
data/lib/htauth/console.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
require "htauth/error"
|
|
3
5
|
|
|
4
6
|
# With many thanks to JEG2 - http://graysoftinc.com/terminal-tricks/random-access-terminal
|
|
5
7
|
#
|
|
6
8
|
module HTAuth
|
|
7
9
|
# Internal: Utility class for managing console input/output
|
|
8
10
|
class Console
|
|
9
|
-
attr_reader :input
|
|
10
|
-
attr_reader :output
|
|
11
|
+
attr_reader :input, :output
|
|
11
12
|
|
|
12
13
|
def initialize(input = $stdin, output = $stdout)
|
|
13
14
|
@input = input
|
|
@@ -23,9 +24,11 @@ module HTAuth
|
|
|
23
24
|
answer = read_answer
|
|
24
25
|
output.puts
|
|
25
26
|
raise ConsoleError, "No input given" if answer.nil?
|
|
27
|
+
|
|
26
28
|
answer.strip!
|
|
27
|
-
raise ConsoleError, "No input given" if answer.
|
|
28
|
-
|
|
29
|
+
raise ConsoleError, "No input given" if answer.empty?
|
|
30
|
+
|
|
31
|
+
answer
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def read_answer
|
data/lib/htauth/crypt.rb
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "htauth/algorithm"
|
|
2
4
|
|
|
3
5
|
module HTAuth
|
|
4
6
|
# Internal: The basic crypt algorithm
|
|
5
7
|
class Crypt < Algorithm
|
|
6
|
-
|
|
7
8
|
ENTRY_LENGTH = 13
|
|
8
|
-
ENTRY_REGEX =
|
|
9
|
+
ENTRY_REGEX = /\A[^$:\s]{#{ENTRY_LENGTH}}\z/
|
|
9
10
|
|
|
10
11
|
def self.handles?(password_entry)
|
|
11
12
|
ENTRY_REGEX.match?(password_entry)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def self.extract_salt_from_existing_password_field(existing)
|
|
15
|
-
existing[0,2]
|
|
16
|
+
existing[0, 2]
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def initialize(params = {})
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
super()
|
|
21
|
+
@salt = if (existing = params["existing"] || params[:existing])
|
|
22
|
+
self.class.extract_salt_from_existing_password_field(existing)
|
|
23
|
+
else
|
|
24
|
+
params[:salt] || params["salt"] || gen_salt
|
|
25
|
+
end
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def encode(password)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module HTAuth
|
|
2
4
|
#
|
|
3
5
|
# Use by either
|
|
@@ -18,28 +20,28 @@ module HTAuth
|
|
|
18
20
|
# them in a Set that is available via the 'children' method.
|
|
19
21
|
#
|
|
20
22
|
module DescendantTracker
|
|
21
|
-
def inherited(
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
def inherited(klass)
|
|
24
|
+
super
|
|
25
|
+
return unless klass.instance_of?(Class)
|
|
26
|
+
|
|
27
|
+
children << klass
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
#
|
|
27
31
|
# The list of children that are registered
|
|
28
32
|
#
|
|
29
33
|
def children
|
|
30
|
-
unless defined? @children
|
|
31
|
-
|
|
32
|
-
end
|
|
33
|
-
return @children
|
|
34
|
+
@children = [] unless defined? @children
|
|
35
|
+
@children
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
#
|
|
37
39
|
# find the child that returns truthy for then given method and
|
|
38
40
|
# parameters
|
|
39
41
|
#
|
|
40
|
-
def find_child(
|
|
42
|
+
def find_child(method, *args)
|
|
41
43
|
children.find do |child|
|
|
42
|
-
child.send(
|
|
44
|
+
child.send(method, *args)
|
|
43
45
|
end
|
|
44
46
|
end
|
|
45
47
|
end
|
data/lib/htauth/digest_entry.rb
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "htauth/error"
|
|
4
|
+
require "htauth/entry"
|
|
5
|
+
require "htauth/algorithm"
|
|
6
|
+
require "digest/md5"
|
|
5
7
|
|
|
6
8
|
module HTAuth
|
|
7
9
|
# Internal: Object version of a single record from an htdigest file
|
|
8
10
|
class DigestEntry
|
|
9
|
-
|
|
10
11
|
# Internal: The user of this entry
|
|
11
12
|
attr_accessor :user
|
|
12
13
|
# Internal: The realm of this entry
|
|
@@ -21,10 +22,10 @@ module HTAuth
|
|
|
21
22
|
#
|
|
22
23
|
# Returns an instance of DigestEntry
|
|
23
24
|
def from_line(line)
|
|
24
|
-
parts =
|
|
25
|
+
parts = entry!(line)
|
|
25
26
|
d = DigestEntry.new(parts[0], parts[1])
|
|
26
27
|
d.digest = parts[2]
|
|
27
|
-
|
|
28
|
+
d
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# Internal: test if the given line is valid for this Entry class
|
|
@@ -37,30 +38,31 @@ module HTAuth
|
|
|
37
38
|
#
|
|
38
39
|
# Returns the individual parts of the line
|
|
39
40
|
# Raises InvalidDigestEntry if it is not a a valid entry
|
|
40
|
-
def
|
|
41
|
-
raise InvalidDigestEntry, "line commented out" if line
|
|
41
|
+
def entry!(line)
|
|
42
|
+
raise InvalidDigestEntry, "line commented out" if line.start_with?("#")
|
|
43
|
+
|
|
42
44
|
parts = line.strip.split(":")
|
|
43
45
|
raise InvalidDigestEntry, "line must be of the format username:realm:md5checksum" if parts.size != 3
|
|
44
|
-
raise InvalidDigestEntry, "md5 checksum is not 32 characters long" if parts.last.size
|
|
45
|
-
raise InvalidDigestEntry, "md5 checksum has invalid characters"
|
|
46
|
-
|
|
46
|
+
raise InvalidDigestEntry, "md5 checksum is not 32 characters long" if parts.last.size != 32
|
|
47
|
+
raise InvalidDigestEntry, "md5 checksum has invalid characters" unless /\A[[:xdigit:]]{32}\Z/.match?(parts.last)
|
|
48
|
+
|
|
49
|
+
parts
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
# Internal: Returns whether or not the line is a valid entry
|
|
50
53
|
#
|
|
51
54
|
# Returns true or false
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return false
|
|
58
|
-
end
|
|
55
|
+
def entry?(line)
|
|
56
|
+
entry!(line)
|
|
57
|
+
true
|
|
58
|
+
rescue InvalidDigestEntry
|
|
59
|
+
false
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
# Internal: Create a new Entry with the given user, realm and password
|
|
63
64
|
def initialize(user, realm, password = "")
|
|
65
|
+
super()
|
|
64
66
|
@user = user
|
|
65
67
|
@realm = realm
|
|
66
68
|
@digest = calc_digest(password)
|
|
@@ -79,7 +81,7 @@ module HTAuth
|
|
|
79
81
|
# Public: Check if the given password is the password of this entry.
|
|
80
82
|
def authenticated?(check_password)
|
|
81
83
|
check = calc_digest(check_password)
|
|
82
|
-
|
|
84
|
+
Algorithm.secure_compare(check, digest)
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
# Internal: Returns the key of this entry
|
data/lib/htauth/digest_file.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "htauth/error"
|
|
5
|
+
require "htauth/file"
|
|
6
|
+
require "htauth/digest_entry"
|
|
5
7
|
|
|
6
8
|
module HTAuth
|
|
7
9
|
# Public: An API for managing an 'htdigest' file
|
|
@@ -15,7 +17,6 @@ module HTAuth
|
|
|
15
17
|
# end
|
|
16
18
|
#
|
|
17
19
|
class DigestFile < HTAuth::File
|
|
18
|
-
|
|
19
20
|
# Private: The class implementing a single entry in the DigestFile
|
|
20
21
|
ENTRY_KLASS = HTAuth::DigestEntry
|
|
21
22
|
|
|
@@ -32,7 +33,7 @@ module HTAuth
|
|
|
32
33
|
# Returns true or false if the username/realm combination is found.
|
|
33
34
|
def has_entry?(username, realm)
|
|
34
35
|
test_entry = DigestEntry.new(username, realm)
|
|
35
|
-
@entries.
|
|
36
|
+
@entries.key?(test_entry.key)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
# Public: remove the given username / realm from the file.
|
|
@@ -48,10 +49,10 @@ module HTAuth
|
|
|
48
49
|
#
|
|
49
50
|
# Returns nothing
|
|
50
51
|
def delete(username, realm)
|
|
51
|
-
if has_entry?(username, realm)
|
|
52
|
+
if has_entry?(username, realm)
|
|
52
53
|
ir = internal_record(username, realm)
|
|
53
|
-
line_index = ir[
|
|
54
|
-
@entries.delete(ir[
|
|
54
|
+
line_index = ir["line_index"]
|
|
55
|
+
@entries.delete(ir["entry"].key)
|
|
55
56
|
@lines[line_index] = nil
|
|
56
57
|
dirty!
|
|
57
58
|
end
|
|
@@ -76,7 +77,7 @@ module HTAuth
|
|
|
76
77
|
#
|
|
77
78
|
# Returns nothing.
|
|
78
79
|
def add_or_update(username, realm, password)
|
|
79
|
-
if has_entry?(username, realm)
|
|
80
|
+
if has_entry?(username, realm)
|
|
80
81
|
update(username, realm, password)
|
|
81
82
|
else
|
|
82
83
|
add(username, realm, password)
|
|
@@ -97,14 +98,16 @@ module HTAuth
|
|
|
97
98
|
# Returns nothing.
|
|
98
99
|
# Raises DigestFileError if the give username / realm already exists.
|
|
99
100
|
def add(username, realm, password)
|
|
100
|
-
raise DigestFileError, "Unable to add already existing user #{username} in realm #{realm}" if has_entry?(
|
|
101
|
+
raise DigestFileError, "Unable to add already existing user #{username} in realm #{realm}" if has_entry?(
|
|
102
|
+
username, realm
|
|
103
|
+
)
|
|
101
104
|
|
|
102
105
|
new_entry = DigestEntry.new(username, realm, password)
|
|
103
106
|
new_index = @lines.size
|
|
104
107
|
@lines << new_entry.to_s
|
|
105
|
-
@entries[new_entry.key] = {
|
|
108
|
+
@entries[new_entry.key] = { "entry" => new_entry, "line_index" => new_index }
|
|
106
109
|
dirty!
|
|
107
|
-
|
|
110
|
+
nil
|
|
108
111
|
end
|
|
109
112
|
|
|
110
113
|
# Public: Updates an existing username / relam entry with a new password
|
|
@@ -121,12 +124,15 @@ module HTAuth
|
|
|
121
124
|
# Returns nothing
|
|
122
125
|
# Raises DigestfileError if the username / realm is not found in the file
|
|
123
126
|
def update(username, realm, password)
|
|
124
|
-
raise DigestFileError, "Unable to update non-existent user #{username} in realm #{realm}" unless has_entry?(
|
|
127
|
+
raise DigestFileError, "Unable to update non-existent user #{username} in realm #{realm}" unless has_entry?(
|
|
128
|
+
username, realm
|
|
129
|
+
)
|
|
130
|
+
|
|
125
131
|
ir = internal_record(username, realm)
|
|
126
|
-
ir[
|
|
127
|
-
@lines[ir[
|
|
132
|
+
ir["entry"].password = password
|
|
133
|
+
@lines[ir["line_index"]] = ir["entry"].to_s
|
|
128
134
|
dirty!
|
|
129
|
-
|
|
135
|
+
nil
|
|
130
136
|
end
|
|
131
137
|
|
|
132
138
|
# Public: Returns the given DigestEntry from the file.
|
|
@@ -145,8 +151,9 @@ module HTAuth
|
|
|
145
151
|
# Returns nil if the entry is not found
|
|
146
152
|
def fetch(username, realm)
|
|
147
153
|
return nil unless has_entry?(username, realm)
|
|
154
|
+
|
|
148
155
|
ir = internal_record(username, realm)
|
|
149
|
-
|
|
156
|
+
ir["entry"].dup
|
|
150
157
|
end
|
|
151
158
|
|
|
152
159
|
# Internal: returns the class used for each entry
|
data/lib/htauth/entry.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module HTAuth
|
|
2
4
|
# Internal: base class from which all entries are derived
|
|
3
5
|
class Entry
|
|
4
6
|
# Internal: return a new instance of this entry
|
|
5
7
|
def dup
|
|
6
|
-
self.class.from_line(
|
|
8
|
+
self.class.from_line(to_s)
|
|
7
9
|
end
|
|
8
10
|
end
|
|
9
11
|
end
|