htauth 1.0.1 → 1.0.2
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.
- data/{CHANGES → HISTORY} +4 -0
- data/README +5 -4
- data/bin/htdigest-ruby +13 -6
- data/bin/htpasswd-ruby +8 -1
- data/gemspec.rb +43 -0
- data/lib/htauth.rb +29 -7
- data/lib/htauth/algorithm.rb +51 -50
- data/lib/htauth/crypt.rb +11 -11
- data/lib/htauth/digest.rb +107 -108
- data/lib/htauth/digest_entry.rb +53 -53
- data/lib/htauth/digest_file.rb +61 -66
- data/lib/htauth/entry.rb +5 -5
- data/lib/htauth/file.rb +84 -84
- data/lib/htauth/md5.rb +72 -72
- data/lib/htauth/passwd.rb +149 -150
- data/lib/htauth/passwd_entry.rb +77 -77
- data/lib/htauth/passwd_file.rb +0 -4
- data/lib/htauth/version.rb +16 -13
- data/tasks/announce.rake +38 -0
- data/tasks/config.rb +98 -0
- data/tasks/distribution.rake +46 -0
- data/tasks/documentation.rake +31 -0
- data/tasks/rspec.rake +29 -0
- data/tasks/rubyforge.rake +59 -0
- data/tasks/utils.rb +80 -0
- metadata +79 -89
- data/lib/htauth/gemspec.rb +0 -53
- data/lib/htauth/specification.rb +0 -129
- 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/lib/htauth/digest_entry.rb
CHANGED
@@ -2,72 +2,72 @@ require 'htauth/entry'
|
|
2
2
|
require 'digest/md5'
|
3
3
|
|
4
4
|
module HTAuth
|
5
|
-
|
5
|
+
class InvalidDigestEntry < StandardError ; end
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
# A single record in an htdigest file.
|
8
|
+
class DigestEntry
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
attr_accessor :user
|
11
|
+
attr_accessor :realm
|
12
|
+
attr_accessor :digest
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
14
|
+
class << self
|
15
|
+
def from_line(line)
|
16
|
+
parts = is_entry!(line)
|
17
|
+
d = DigestEntry.new(parts[0], parts[1])
|
18
|
+
d.digest = parts[2]
|
19
|
+
return d
|
20
|
+
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
22
|
+
# test if a line is an entry, raise InvalidDigestEntry if it is not.
|
23
|
+
# an entry must be composed of 3 parts, username:realm:md5sum
|
24
|
+
# where username, and realm do not contain the ':' character
|
25
|
+
# and the md5sum must be 32 characters long.
|
26
|
+
def is_entry!(line)
|
27
|
+
raise InvalidDigestEntry, "line commented out" if line =~ /\A#/
|
28
|
+
parts = line.strip.split(":")
|
29
|
+
raise InvalidDigestEntry, "line must be of the format username:realm:md5checksum" if parts.size != 3
|
30
|
+
raise InvalidDigestEntry, "md5 checksum is not 32 characters long" if parts.last.size != 32
|
31
|
+
raise InvalidDigestEntry, "md5 checksum has invalid characters" if parts.last !~ /\A[[:xdigit:]]{32}\Z/
|
32
|
+
return parts
|
33
|
+
end
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
43
|
-
end
|
35
|
+
# test if a line is an entry and return true or false
|
36
|
+
def is_entry?(line)
|
37
|
+
begin
|
38
|
+
is_entry!(line)
|
39
|
+
return true
|
40
|
+
rescue InvalidDigestEntry
|
41
|
+
return false
|
44
42
|
end
|
43
|
+
end
|
44
|
+
end
|
45
45
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
46
|
+
def initialize(user, realm, password = "")
|
47
|
+
@user = user
|
48
|
+
@realm = realm
|
49
|
+
@digest = calc_digest(password)
|
50
|
+
end
|
51
51
|
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
def password=(new_password)
|
53
|
+
@digest = calc_digest(new_password)
|
54
|
+
end
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
def calc_digest(password)
|
57
|
+
::Digest::MD5.hexdigest("#{user}:#{realm}:#{password}")
|
58
|
+
end
|
59
59
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
60
|
+
def authenticated?(check_password)
|
61
|
+
hd = ::Digest::MD5.hexdigest("#{user}:#{realm}:#{check_password}")
|
62
|
+
return hd == digest
|
63
|
+
end
|
64
64
|
|
65
|
-
|
65
|
+
def key
|
66
66
|
"#{user}:#{realm}"
|
67
|
-
|
67
|
+
end
|
68
68
|
|
69
|
-
|
69
|
+
def to_s
|
70
70
|
"#{user}:#{realm}:#{digest}"
|
71
|
-
end
|
72
71
|
end
|
72
|
+
end
|
73
73
|
end
|
data/lib/htauth/digest_file.rb
CHANGED
@@ -1,85 +1,80 @@
|
|
1
1
|
require 'stringio'
|
2
|
-
require 'tempfile'
|
3
2
|
|
4
3
|
require 'htauth/file'
|
5
4
|
require 'htauth/digest_entry'
|
6
5
|
|
7
6
|
module HTAuth
|
8
|
-
|
9
|
-
|
7
|
+
class DigestFileError < StandardError ; end
|
8
|
+
class DigestFile < HTAuth::File
|
10
9
|
|
11
|
-
|
10
|
+
ENTRY_KLASS = HTAuth::DigestEntry
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
# does the entry the the specified username and realm exist in the file
|
13
|
+
def has_entry?(username, realm)
|
14
|
+
test_entry = DigestEntry.new(username, realm)
|
15
|
+
@entries.has_key?(test_entry.key)
|
16
|
+
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
18
|
+
# remove an entry from the file
|
19
|
+
def delete(username, realm)
|
20
|
+
if has_entry?(username, realm) then
|
21
|
+
ir = internal_record(username, realm)
|
22
|
+
line_index = ir['line_index']
|
23
|
+
@entries.delete(ir['entry'].key)
|
24
|
+
@lines[line_index] = nil
|
25
|
+
dirty!
|
26
|
+
end
|
27
|
+
nil
|
28
|
+
end
|
30
29
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
30
|
+
# add or update an entry as appropriate
|
31
|
+
def add_or_update(username, realm, password)
|
32
|
+
if has_entry?(username, realm) then
|
33
|
+
update(username, realm, password)
|
34
|
+
else
|
35
|
+
add(username, realm, password)
|
36
|
+
end
|
37
|
+
end
|
39
38
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
new_entry = DigestEntry.new(username, realm, password)
|
45
|
-
new_index = @lines.size
|
46
|
-
@lines << new_entry.to_s
|
47
|
-
@entries[new_entry.key] = { 'entry' => new_entry, 'line_index' => new_index }
|
48
|
-
dirty!
|
49
|
-
return nil
|
50
|
-
end
|
39
|
+
# add an new record. raises an error if the entry exists.
|
40
|
+
def add(username, realm, password)
|
41
|
+
raise DigestFileError, "Unable to add already existing user #{username} in realm #{realm}" if has_entry?(username, realm)
|
51
42
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
return nil
|
60
|
-
end
|
43
|
+
new_entry = DigestEntry.new(username, realm, password)
|
44
|
+
new_index = @lines.size
|
45
|
+
@lines << new_entry.to_s
|
46
|
+
@entries[new_entry.key] = { 'entry' => new_entry, 'line_index' => new_index }
|
47
|
+
dirty!
|
48
|
+
return nil
|
49
|
+
end
|
61
50
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
51
|
+
# update an already existing entry with a new password. raises an error if the entry does not exist
|
52
|
+
def update(username, realm, password)
|
53
|
+
raise DigestFileError, "Unable to update non-existent user #{username} in realm #{realm}" unless has_entry?(username, realm)
|
54
|
+
ir = internal_record(username, realm)
|
55
|
+
ir['entry'].password = password
|
56
|
+
@lines[ir['line_index']] = ir['entry'].to_s
|
57
|
+
dirty!
|
58
|
+
return nil
|
59
|
+
end
|
69
60
|
|
70
|
-
|
71
|
-
|
72
|
-
|
61
|
+
# fetches a copy of an entry from the file. Updateing the entry returned from fetch will NOT
|
62
|
+
# propogate back to the file.
|
63
|
+
def fetch(username, realm)
|
64
|
+
return nil unless has_entry?(username, realm)
|
65
|
+
ir = internal_record(username, realm)
|
66
|
+
return ir['entry'].dup
|
67
|
+
end
|
73
68
|
|
74
|
-
|
75
|
-
|
76
|
-
|
69
|
+
def entry_klass
|
70
|
+
ENTRY_KLASS
|
71
|
+
end
|
77
72
|
|
78
|
-
|
73
|
+
private
|
79
74
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
end
|
75
|
+
def internal_record(username, realm)
|
76
|
+
e = DigestEntry.new(username, realm)
|
77
|
+
@entries[e.key]
|
84
78
|
end
|
79
|
+
end
|
85
80
|
end
|
data/lib/htauth/entry.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module HTAuth
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
3
|
+
# base class from which all entries are derived
|
4
|
+
class Entry
|
5
|
+
def dup
|
6
|
+
self.class.from_line(self.to_s)
|
8
7
|
end
|
8
|
+
end
|
9
9
|
end
|
data/lib/htauth/file.rb
CHANGED
@@ -2,102 +2,102 @@ require 'stringio'
|
|
2
2
|
require 'htauth'
|
3
3
|
|
4
4
|
module HTAuth
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
class FileAccessError < StandardError ; end
|
6
|
+
class File
|
7
|
+
ALTER = "alter"
|
8
|
+
CREATE = "create"
|
9
|
+
STDOUT_FLAG = "-"
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
attr_reader :filename
|
12
|
+
attr_reader :file
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
return f
|
27
|
-
end
|
14
|
+
class << self
|
15
|
+
# open a file yielding the the file object for use. The file is saved when
|
16
|
+
# the block exists, if the file has had alterations made.
|
17
|
+
def open(filename, mode = ALTER)
|
18
|
+
f = self.new(filename, mode)
|
19
|
+
if block_given?
|
20
|
+
begin
|
21
|
+
yield f
|
22
|
+
ensure
|
23
|
+
f.save! if f and f.dirty?
|
24
|
+
end
|
28
25
|
end
|
26
|
+
return f
|
27
|
+
end
|
28
|
+
end
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
raise FileAccessError, "Invalid mode #{mode}" unless [ ALTER, CREATE ].include?(mode)
|
30
|
+
# Create or Alter a password file.
|
31
|
+
#
|
32
|
+
# Altering a non-existent file is an error. Creating an existing file results in
|
33
|
+
# a truncation and overwrite of the existing file.
|
34
|
+
def initialize(filename, mode = ALTER)
|
35
|
+
@filename = filename
|
36
|
+
@mode = mode
|
37
|
+
@dirty = false
|
40
38
|
|
41
|
-
|
42
|
-
raise FileAccessError, "Could not open passwd file #{filename} for reading."
|
43
|
-
end
|
39
|
+
raise FileAccessError, "Invalid mode #{mode}" unless [ ALTER, CREATE ].include?(mode)
|
44
40
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
load_entries if (@mode == ALTER) and (filename != STDOUT_FLAG)
|
49
|
-
rescue => e
|
50
|
-
raise FileAccessError, e.message
|
51
|
-
end
|
52
|
-
end
|
41
|
+
if (filename != STDOUT_FLAG) and (mode == ALTER) and (not ::File.exist?(filename)) then
|
42
|
+
raise FileAccessError, "Could not open passwd file #{filename} for reading."
|
43
|
+
end
|
53
44
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
45
|
+
begin
|
46
|
+
@entries = {}
|
47
|
+
@lines = []
|
48
|
+
load_entries if (@mode == ALTER) and (filename != STDOUT_FLAG)
|
49
|
+
rescue => e
|
50
|
+
raise FileAccessError, e.message
|
51
|
+
end
|
52
|
+
end
|
58
53
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
54
|
+
# return whether or not an alteration to the file has happened
|
55
|
+
def dirty?
|
56
|
+
@dirty
|
57
|
+
end
|
63
58
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
when STDOUT_FLAG
|
69
|
-
$stdout.write(contents)
|
70
|
-
else
|
71
|
-
::File.open(@filename,"w") do |f|
|
72
|
-
f.write(contents)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
@dirty = false
|
76
|
-
rescue => e
|
77
|
-
raise FileAccessError, "Error saving file #{@filename} : #{e.message}"
|
78
|
-
end
|
79
|
-
end
|
59
|
+
# mark the file as dirty
|
60
|
+
def dirty!
|
61
|
+
@dirty = true
|
62
|
+
end
|
80
63
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
64
|
+
# update the original file with the new contents
|
65
|
+
def save!
|
66
|
+
begin
|
67
|
+
case filename
|
68
|
+
when STDOUT_FLAG
|
69
|
+
$stdout.write(contents)
|
70
|
+
else
|
71
|
+
::File.open(@filename,"w") do |f|
|
72
|
+
f.write(contents)
|
73
|
+
end
|
88
74
|
end
|
75
|
+
@dirty = false
|
76
|
+
rescue => e
|
77
|
+
raise FileAccessError, "Error saving file #{@filename} : #{e.message}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# return what should be the contents of the file
|
82
|
+
def contents
|
83
|
+
c = StringIO.new
|
84
|
+
@lines.each do |l|
|
85
|
+
c.puts l if l
|
86
|
+
end
|
87
|
+
c.string
|
88
|
+
end
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
end
|
100
|
-
end
|
90
|
+
# load up entries, keep items in the same order and do not trim out any
|
91
|
+
# items in the file, like commented out lines or empty space
|
92
|
+
def load_entries
|
93
|
+
@lines = IO.readlines(@filename)
|
94
|
+
@lines.each_with_index do |line,idx|
|
95
|
+
if entry_klass.is_entry?(line) then
|
96
|
+
entry = entry_klass.from_line(line)
|
97
|
+
v = { 'entry' => entry, 'line_index' => idx }
|
98
|
+
@entries[entry.key] = v
|
101
99
|
end
|
100
|
+
end
|
102
101
|
end
|
102
|
+
end
|
103
103
|
end
|