htauth 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|