htauth 1.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.
- data/CHANGES +4 -0
- data/LICENSE +19 -0
- data/README +94 -0
- data/bin/htdigest-ruby +12 -0
- data/bin/htpasswd-ruby +12 -0
- data/lib/htauth/algorithm.rb +67 -0
- data/lib/htauth/crypt.rb +20 -0
- data/lib/htauth/digest.rb +128 -0
- data/lib/htauth/digest_entry.rb +72 -0
- data/lib/htauth/digest_file.rb +85 -0
- data/lib/htauth/entry.rb +9 -0
- data/lib/htauth/file.rb +102 -0
- data/lib/htauth/gemspec.rb +52 -0
- data/lib/htauth/md5.rb +82 -0
- data/lib/htauth/passwd.rb +174 -0
- data/lib/htauth/passwd_entry.rb +97 -0
- data/lib/htauth/passwd_file.rb +86 -0
- data/lib/htauth/plaintext.rb +18 -0
- data/lib/htauth/sha1.rb +23 -0
- data/lib/htauth/specification.rb +128 -0
- data/lib/htauth/version.rb +18 -0
- data/lib/htauth.rb +27 -0
- data/spec/crypt_spec.rb +18 -0
- data/spec/digest_entry_spec.rb +61 -0
- data/spec/digest_file_spec.rb +66 -0
- data/spec/digest_spec.rb +150 -0
- data/spec/md5_spec.rb +17 -0
- data/spec/passwd_entry_spec.rb +139 -0
- data/spec/passwd_file_spec.rb +67 -0
- data/spec/passwd_spec.rb +208 -0
- data/spec/plaintext_spec.rb +18 -0
- data/spec/sha1_spec.rb +17 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/test.add.digest +3 -0
- data/spec/test.add.passwd +3 -0
- data/spec/test.delete.digest +1 -0
- data/spec/test.delete.passwd +1 -0
- data/spec/test.original.digest +2 -0
- data/spec/test.original.passwd +2 -0
- data/spec/test.update.digest +2 -0
- data/spec/test.update.passwd +2 -0
- metadata +122 -0
data/CHANGES
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2008 Jeremy Hinegardner
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
== HTAuth
|
2
|
+
|
3
|
+
* Homepage[http://copiousfreetime.rubyforge.org/htauth]
|
4
|
+
* {Rubyforge Project}[http://rubyforge.org/projects/copiousfreetime/]
|
5
|
+
* email jeremy at hinegardner dot org
|
6
|
+
|
7
|
+
== DESCRIPTION
|
8
|
+
|
9
|
+
HTAuth is a pure ruby replacement for the Apache support programs +htdigest+ and
|
10
|
+
+htpasswd+. Command line and API access are provided for access to htdigest and
|
11
|
+
htpasswd files.
|
12
|
+
|
13
|
+
== FEATURES
|
14
|
+
|
15
|
+
Rpassword provides to drop in commands *htdigest-ruby* and *htpasswd-ruby* that
|
16
|
+
can manipulate the digest and passwd files in the same manner as Apache's
|
17
|
+
original commands.
|
18
|
+
|
19
|
+
*htdigest-ruby* and *htpasswd-ruby* are command line compatible with *htdigest*
|
20
|
+
and *htpasswd*. They support the same exact same command line options as the
|
21
|
+
originals, and have some extras.
|
22
|
+
|
23
|
+
Additionally, you can access all the functionality of *htdigest-ruby* and
|
24
|
+
*htpasswd-ruby* through an API.
|
25
|
+
|
26
|
+
== SYNOPSIS
|
27
|
+
|
28
|
+
* htpasswd-ruby command line application
|
29
|
+
|
30
|
+
Usage:
|
31
|
+
htpasswd-ruby [-cmdpsD] passwordfile username
|
32
|
+
htpasswd-ruby -b[cmdpsD] passwordfile username password
|
33
|
+
|
34
|
+
htpasswd-ruby -n[mdps] username
|
35
|
+
htpasswd-ruby -nb[mdps] username password
|
36
|
+
|
37
|
+
-b, --batch Batch mode, get the password from the command line, rather than prompt
|
38
|
+
-c, --create Create a new file; this overwrites an existing file.
|
39
|
+
-d, --crypt Force CRYPT encryption of the password (default).
|
40
|
+
-D, --delete Delete the specified user.
|
41
|
+
-h, --help Display this help.
|
42
|
+
-m, --md5 Force MD5 encryption of the password (default on Windows).
|
43
|
+
-n, --stdout Do not update the file; Display the results on stdout instead.
|
44
|
+
-p, --plaintext Do not encrypt the password (plaintext).
|
45
|
+
-s, --sha1 Force SHA encryption of the password.
|
46
|
+
-v, --version Show version info.
|
47
|
+
|
48
|
+
* htdigest-ruby command line application
|
49
|
+
|
50
|
+
Usage: htdigest-ruby [options] passwordfile realm username
|
51
|
+
-c, --create Create a new digest password file; this overwrites an existing file.
|
52
|
+
-D, --delete Delete the specified user.
|
53
|
+
-h, --help Display this help.
|
54
|
+
-v, --version Show version info.
|
55
|
+
|
56
|
+
* API Usage
|
57
|
+
|
58
|
+
HTAuth::DigestFile.new("some.htdigest") do |df|
|
59
|
+
df.add_or_update('someuser', 'myrealm', 'a password')
|
60
|
+
df.delete('someolduser', 'myotherrealm')
|
61
|
+
end
|
62
|
+
|
63
|
+
HTAuth::PasswdFile.new("some.htpasswd", HTAuth::File::CREATE) do |pf|
|
64
|
+
pf.add('someuser', 'a password', 'md5')
|
65
|
+
pf.add('someotheruser', 'a different password', 'sha1')
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
== CREDITS
|
70
|
+
|
71
|
+
* {The Apache Software Foundation}[http://www.apache.org/]
|
72
|
+
* all the folks who contributed to htdigest and htpassword
|
73
|
+
|
74
|
+
== LICENSE
|
75
|
+
|
76
|
+
Copyright (c) 2008 Jeremy Hinegardner
|
77
|
+
|
78
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
79
|
+
this software and associated documentation files (the "Software"), to deal in
|
80
|
+
the Software without restriction, including without limitation the rights to
|
81
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
82
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
83
|
+
so, subject to the following conditions:
|
84
|
+
|
85
|
+
The above copyright notice and this permission notice shall be included in all
|
86
|
+
copies or substantial portions of the Software.
|
87
|
+
|
88
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
89
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
90
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
91
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
92
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
93
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
94
|
+
SOFTWARE.
|
data/bin/htdigest-ruby
ADDED
data/bin/htpasswd-ruby
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
|
2
|
+
module HTAuth
|
3
|
+
class InvalidAlgorithmError < StandardError ; end
|
4
|
+
# base class all the Passwd algorithms derive from
|
5
|
+
class Algorithm
|
6
|
+
|
7
|
+
SALT_CHARS = (%w[ . / ] + ("0".."9").to_a + ('A'..'Z').to_a + ('a'..'z').to_a).freeze
|
8
|
+
DEFAULT = ( RUBY_PLATFORM !~ /mswin32/ ) ? "crypt" : "md5"
|
9
|
+
EXISTING = "existing"
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def algorithm_from_name(a_name, params = {})
|
13
|
+
raise InvalidAlgorithmError, "`#{a_name}' is an invalid encryption algorithm, use one of #{sub_klasses.keys.join(', ')}" unless sub_klasses[a_name.downcase]
|
14
|
+
sub_klasses[a_name.downcase].new(params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def algorithms_from_field(password_field)
|
18
|
+
matches = []
|
19
|
+
|
20
|
+
if password_field.index(sub_klasses['sha1'].new.prefix) then
|
21
|
+
matches << sub_klasses['sha1'].new
|
22
|
+
elsif password_field.index(sub_klasses['md5'].new.prefix) then
|
23
|
+
p = password_field.split("$")
|
24
|
+
matches << sub_klasses['md5'].new( :salt => p[2] )
|
25
|
+
else
|
26
|
+
matches << sub_klasses['plaintext'].new
|
27
|
+
matches << sub_klasses['crypt'].new( :salt => password_field[0,2] )
|
28
|
+
end
|
29
|
+
|
30
|
+
return matches
|
31
|
+
end
|
32
|
+
|
33
|
+
def inherited(sub_klass)
|
34
|
+
k = sub_klass.name.split("::").last.downcase
|
35
|
+
sub_klasses[k] = sub_klass
|
36
|
+
end
|
37
|
+
|
38
|
+
def sub_klasses
|
39
|
+
@sub_klasses ||= {}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def prefix ; end
|
44
|
+
def encode(password) ; end
|
45
|
+
|
46
|
+
# 8 bytes of random items from SALT_CHARS
|
47
|
+
def gen_salt
|
48
|
+
chars = []
|
49
|
+
8.times { chars << SALT_CHARS[rand(SALT_CHARS.size)] }
|
50
|
+
chars.join('')
|
51
|
+
end
|
52
|
+
|
53
|
+
# this is not the Base64 encoding, this is the to64() method from apr
|
54
|
+
def to_64(number, rounds)
|
55
|
+
r = StringIO.new
|
56
|
+
rounds.times do |x|
|
57
|
+
r.print(SALT_CHARS[number % 64])
|
58
|
+
number >>= 6
|
59
|
+
end
|
60
|
+
return r.string
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
require 'htauth/md5'
|
65
|
+
require 'htauth/sha1'
|
66
|
+
require 'htauth/crypt'
|
67
|
+
require 'htauth/plaintext'
|
data/lib/htauth/crypt.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'htauth/algorithm'
|
2
|
+
|
3
|
+
module HTAuth
|
4
|
+
|
5
|
+
# The basic crypt algorithm
|
6
|
+
class Crypt < Algorithm
|
7
|
+
|
8
|
+
def initialize(params = {})
|
9
|
+
@salt = params[:salt] || params['salt'] || gen_salt
|
10
|
+
end
|
11
|
+
|
12
|
+
def prefix
|
13
|
+
""
|
14
|
+
end
|
15
|
+
|
16
|
+
def encode(password)
|
17
|
+
password.crypt(@salt)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'htauth/digest_file'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'highline'
|
7
|
+
|
8
|
+
module HTAuth
|
9
|
+
class Digest
|
10
|
+
|
11
|
+
MAX_PASSWD_LENGTH = 255
|
12
|
+
|
13
|
+
attr_accessor :digest_file
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@digest_file = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def options
|
20
|
+
if @options.nil? then
|
21
|
+
@options = ::OpenStruct.new
|
22
|
+
@options.show_version = false
|
23
|
+
@options.show_help = false
|
24
|
+
@options.file_mode = DigestFile::ALTER
|
25
|
+
@options.passwdfile = nil
|
26
|
+
@options.realm = nil
|
27
|
+
@options.username = nil
|
28
|
+
@options.delete_entry = false
|
29
|
+
end
|
30
|
+
@options
|
31
|
+
end
|
32
|
+
|
33
|
+
def option_parser
|
34
|
+
if not @option_parser then
|
35
|
+
@option_parser = OptionParser.new do |op|
|
36
|
+
op.banner = "Usage: #{op.program_name} [options] passwordfile realm username"
|
37
|
+
op.on("-c", "--create", "Create a new digest password file; this overwrites an existing file.") do |c|
|
38
|
+
options.file_mode = DigestFile::CREATE
|
39
|
+
end
|
40
|
+
|
41
|
+
op.on("-D", "--delete", "Delete the specified user.") do |d|
|
42
|
+
options.delete_entry = d
|
43
|
+
end
|
44
|
+
|
45
|
+
op.on("-h", "--help", "Display this help.") do |h|
|
46
|
+
options.show_help = h
|
47
|
+
end
|
48
|
+
|
49
|
+
op.on("-v", "--version", "Show version info.") do |v|
|
50
|
+
options.show_version = v
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
@option_parser
|
55
|
+
end
|
56
|
+
|
57
|
+
def show_help
|
58
|
+
$stdout.puts option_parser
|
59
|
+
exit 1
|
60
|
+
end
|
61
|
+
|
62
|
+
def show_version
|
63
|
+
$stdout.puts "#{option_parser.program_name}: version #{HTAuth::VERSION}"
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_options(argv)
|
68
|
+
begin
|
69
|
+
option_parser.parse!(argv)
|
70
|
+
show_version if options.show_version
|
71
|
+
show_help if options.show_help or argv.size < 3
|
72
|
+
|
73
|
+
options.passwdfile = argv.shift
|
74
|
+
options.realm = argv.shift
|
75
|
+
options.username = argv.shift
|
76
|
+
rescue ::OptionParser::ParseError => pe
|
77
|
+
$stderr.puts "ERROR: #{option_parser.program_name} - #{pe}"
|
78
|
+
$stderr.puts "Try `#{option_parser.program_name} --help` for more information"
|
79
|
+
exit 1
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def run(argv)
|
84
|
+
begin
|
85
|
+
parse_options(argv)
|
86
|
+
digest_file = DigestFile.new(options.passwdfile, options.file_mode)
|
87
|
+
|
88
|
+
if options.delete_entry then
|
89
|
+
digest_file.delete(options.username, options.realm)
|
90
|
+
else
|
91
|
+
# initialize here so that if $stdin is overwritten it gest picked up
|
92
|
+
hl = ::HighLine.new
|
93
|
+
|
94
|
+
action = digest_file.has_entry?(options.username, options.realm) ? "Changing" : "Adding"
|
95
|
+
|
96
|
+
$stdout.puts "#{action} password for #{options.username} in realm #{options.realm}."
|
97
|
+
|
98
|
+
pw_in = hl.ask(" New password: ") { |q| q.echo = '*' }
|
99
|
+
raise PasswordError, "password '#{pw_in}' too long" if pw_in.length >= MAX_PASSWD_LENGTH
|
100
|
+
|
101
|
+
pw_validate = hl.ask("Re-type new password: ") { |q| q.echo = '*' }
|
102
|
+
raise PasswordError, "They don't match, sorry." unless pw_in == pw_validate
|
103
|
+
|
104
|
+
digest_file.add_or_update(options.username, options.realm, pw_in)
|
105
|
+
end
|
106
|
+
|
107
|
+
digest_file.save!
|
108
|
+
|
109
|
+
rescue HTAuth::FileAccessError => fae
|
110
|
+
msg = "Could not open password file #{options.passwdfile} "
|
111
|
+
$stderr.puts "#{msg}: #{fae.message}"
|
112
|
+
$stderr.puts fae.backtrace.join("\n")
|
113
|
+
exit 1
|
114
|
+
rescue HTAuth::PasswordError => pe
|
115
|
+
$stderr.puts "#{pe.message}"
|
116
|
+
exit 1
|
117
|
+
rescue HTAuth::DigestFileError => fe
|
118
|
+
$stderr.puts "#{fe.message}"
|
119
|
+
exit 1
|
120
|
+
rescue SignalException => se
|
121
|
+
$stderr.puts
|
122
|
+
$stderr.puts "Interrupted"
|
123
|
+
exit 1
|
124
|
+
end
|
125
|
+
exit 0
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module HTAuth
|
4
|
+
class InvalidDigestEntry < StandardError ; end
|
5
|
+
|
6
|
+
# A single record in an htdigest file.
|
7
|
+
class DigestEntry
|
8
|
+
|
9
|
+
attr_accessor :user
|
10
|
+
attr_accessor :realm
|
11
|
+
attr_accessor :digest
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def from_line(line)
|
15
|
+
parts = is_entry!(line)
|
16
|
+
d = DigestEntry.new(parts[0], parts[1])
|
17
|
+
d.digest = parts[2]
|
18
|
+
return d
|
19
|
+
end
|
20
|
+
|
21
|
+
# test if a line is an entry, raise InvalidDigestEntry if it is not.
|
22
|
+
# an entry must be composed of 3 parts, username:realm:md5sum
|
23
|
+
# where username, and realm do not contain the ':' character
|
24
|
+
# and the md5sum must be 32 characters long.
|
25
|
+
def is_entry!(line)
|
26
|
+
raise InvalidDigestEntry, "line commented out" if line =~ /\A#/
|
27
|
+
parts = line.strip.split(":")
|
28
|
+
raise InvalidDigestEntry, "line must be of the format username:realm:md5checksum" if parts.size != 3
|
29
|
+
raise InvalidDigestEntry, "md5 checksum is not 32 characters long" if parts.last.size != 32
|
30
|
+
raise InvalidDigestEntry, "md5 checksum has invalid characters" if parts.last !~ /\A[[:xdigit:]]{32}\Z/
|
31
|
+
return parts
|
32
|
+
end
|
33
|
+
|
34
|
+
# test if a line is an entry and return true or false
|
35
|
+
def is_entry?(line)
|
36
|
+
begin
|
37
|
+
is_entry!(line)
|
38
|
+
return true
|
39
|
+
rescue InvalidDigestEntry
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(user, realm, password = "")
|
46
|
+
@user = user
|
47
|
+
@realm = realm
|
48
|
+
@digest = calc_digest(password)
|
49
|
+
end
|
50
|
+
|
51
|
+
def password=(new_password)
|
52
|
+
@digest = calc_digest(new_password)
|
53
|
+
end
|
54
|
+
|
55
|
+
def calc_digest(password)
|
56
|
+
::Digest::MD5.hexdigest("#{user}:#{realm}:#{password}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def authenticated?(check_password)
|
60
|
+
hd = ::Digest::MD5.hexdigest("#{user}:#{realm}:#{check_password}")
|
61
|
+
return hd == digest
|
62
|
+
end
|
63
|
+
|
64
|
+
def key
|
65
|
+
"#{user}:#{realm}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
"#{user}:#{realm}:#{digest}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
require 'htauth/file'
|
5
|
+
require 'htauth/digest_entry'
|
6
|
+
|
7
|
+
module HTAuth
|
8
|
+
class DigestFileError < StandardError ; end
|
9
|
+
class DigestFile < HTAuth::File
|
10
|
+
|
11
|
+
ENTRY_KLASS = HTAuth::DigestEntry
|
12
|
+
|
13
|
+
# does the entry the the specified username and realm exist in the file
|
14
|
+
def has_entry?(username, realm)
|
15
|
+
test_entry = DigestEntry.new(username, realm)
|
16
|
+
@entries.has_key?(test_entry.key)
|
17
|
+
end
|
18
|
+
|
19
|
+
# remove an entry from the file
|
20
|
+
def delete(username, realm)
|
21
|
+
if has_entry?(username, realm) then
|
22
|
+
ir = internal_record(username, realm)
|
23
|
+
line_index = ir['line_index']
|
24
|
+
@entries.delete(ir['entry'].key)
|
25
|
+
@lines[line_index] = nil
|
26
|
+
dirty!
|
27
|
+
end
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# add or update an entry as appropriate
|
32
|
+
def add_or_update(username, realm, password)
|
33
|
+
if has_entry?(username, realm) then
|
34
|
+
update(username, realm, password)
|
35
|
+
else
|
36
|
+
add(username, realm, password)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# add an new record. raises an error if the entry exists.
|
41
|
+
def add(username, realm, password)
|
42
|
+
raise DigestFileError, "Unable to add already existing user #{username} in realm #{realm}" if has_entry?(username, realm)
|
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
|
51
|
+
|
52
|
+
# update an already existing entry with a new password. raises an error if the entry does not exist
|
53
|
+
def update(username, realm, password)
|
54
|
+
raise DigestFileError, "Unable to update non-existent user #{username} in realm #{realm}" unless has_entry?(username, realm)
|
55
|
+
ir = internal_record(username, realm)
|
56
|
+
ir['entry'].password = password
|
57
|
+
@lines[ir['line_index']] = ir['entry'].to_s
|
58
|
+
dirty!
|
59
|
+
return nil
|
60
|
+
end
|
61
|
+
|
62
|
+
# fetches a copy of an entry from the file. Updateing the entry returned from fetch will NOT
|
63
|
+
# propogate back to the file.
|
64
|
+
def fetch(username, realm)
|
65
|
+
return nil unless has_entry?(username, realm)
|
66
|
+
ir = internal_record(username, realm)
|
67
|
+
return ir['entry'].dup
|
68
|
+
end
|
69
|
+
|
70
|
+
def entry_klass
|
71
|
+
ENTRY_KLASS
|
72
|
+
end
|
73
|
+
|
74
|
+
def file_type
|
75
|
+
"digest"
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def internal_record(username, realm)
|
81
|
+
e = DigestEntry.new(username, realm)
|
82
|
+
@entries[e.key]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/htauth/entry.rb
ADDED
data/lib/htauth/file.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module HTAuth
|
4
|
+
class FileAccessError < StandardError ; end
|
5
|
+
class File
|
6
|
+
ALTER = "alter"
|
7
|
+
CREATE = "create"
|
8
|
+
STDOUT_FLAG = "-"
|
9
|
+
|
10
|
+
attr_reader :filename
|
11
|
+
attr_reader :file
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# open a file yielding the the file object for use. The file is saved when
|
15
|
+
# the block exists, if the file has had alterations made.
|
16
|
+
def open(filename, mode = ALTER)
|
17
|
+
f = self.new(filename, mode)
|
18
|
+
if block_given?
|
19
|
+
begin
|
20
|
+
yield f
|
21
|
+
ensure
|
22
|
+
f.save! if f and f.dirty?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
return f
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create or Alter a password file.
|
30
|
+
#
|
31
|
+
# Altering a non-existent file is an error. Creating an existing file results in
|
32
|
+
# a truncation and overwrite of the existing file.
|
33
|
+
def initialize(filename, mode = ALTER)
|
34
|
+
@filename = filename
|
35
|
+
@mode = mode
|
36
|
+
@dirty = false
|
37
|
+
|
38
|
+
raise FileAccessError, "Invalid mode #{mode}" unless [ ALTER, CREATE ].include?(mode)
|
39
|
+
|
40
|
+
if (filename != STDOUT_FLAG) and (mode == ALTER) and (not ::File.exist?(filename)) then
|
41
|
+
raise FileAccessError, "Could not open passwd file #{filename} for reading."
|
42
|
+
end
|
43
|
+
|
44
|
+
begin
|
45
|
+
@entries = {}
|
46
|
+
@lines = []
|
47
|
+
load_entries if (@mode == ALTER) and (filename != STDOUT_FLAG)
|
48
|
+
rescue => e
|
49
|
+
raise FileAccessError, e.message
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# return whether or not an alteration to the file has happened
|
54
|
+
def dirty?
|
55
|
+
@dirty
|
56
|
+
end
|
57
|
+
|
58
|
+
# mark the file as dirty
|
59
|
+
def dirty!
|
60
|
+
@dirty = true
|
61
|
+
end
|
62
|
+
|
63
|
+
# update the original file with the new contents
|
64
|
+
def save!
|
65
|
+
begin
|
66
|
+
case filename
|
67
|
+
when STDOUT_FLAG
|
68
|
+
$stdout.write(contents)
|
69
|
+
else
|
70
|
+
::File.open(@filename,"w") do |f|
|
71
|
+
f.write(contents)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
@dirty = false
|
75
|
+
rescue => e
|
76
|
+
raise FileAccessError, "Error saving file #{@filename} : #{e.message}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# return what should be the contents of the file
|
81
|
+
def contents
|
82
|
+
c = StringIO.new
|
83
|
+
@lines.each do |l|
|
84
|
+
c.puts l if l
|
85
|
+
end
|
86
|
+
c.string
|
87
|
+
end
|
88
|
+
|
89
|
+
# load up entries, keep items in the same order and do not trim out any
|
90
|
+
# items in the file, like commented out lines or empty space
|
91
|
+
def load_entries
|
92
|
+
@lines = IO.readlines(@filename)
|
93
|
+
@lines.each_with_index do |line,idx|
|
94
|
+
if entry_klass.is_entry?(line) then
|
95
|
+
entry = entry_klass.from_line(line)
|
96
|
+
v = { 'entry' => entry, 'line_index' => idx }
|
97
|
+
@entries[entry.key] = v
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'htauth/specification'
|
3
|
+
require 'htauth/version'
|
4
|
+
require 'rake'
|
5
|
+
|
6
|
+
# The Gem Specification plus some extras for htauth.
|
7
|
+
module HTAuth
|
8
|
+
SPEC = HTAuth::Specification.new do |spec|
|
9
|
+
spec.name = "htauth"
|
10
|
+
spec.version = HTAuth::VERSION
|
11
|
+
spec.rubyforge_project = "copiousfreetime"
|
12
|
+
spec.author = "Jeremy Hinegardner"
|
13
|
+
spec.email = "jeremy@hinegardner.org"
|
14
|
+
spec.homepage = "http://copiousfreetime.rubyforge.org/htauth"
|
15
|
+
|
16
|
+
spec.summary = "HTAuth provides htdigest and htpasswd support."
|
17
|
+
spec.description = <<-DESC
|
18
|
+
HTAuth is a pure ruby replacement for the Apache support programs htdigest
|
19
|
+
and htpasswd. Command line and API access are provided for access to
|
20
|
+
htdigest and htpasswd files.
|
21
|
+
DESC
|
22
|
+
|
23
|
+
spec.extra_rdoc_files = FileList["CHANGES", "LICENSE", "README"]
|
24
|
+
spec.has_rdoc = true
|
25
|
+
spec.rdoc_main = "README"
|
26
|
+
spec.rdoc_options = [ "--line-numbers" , "--inline-source" ]
|
27
|
+
|
28
|
+
spec.test_files = FileList["spec/**/*"]
|
29
|
+
spec.executables << "htdigest-ruby"
|
30
|
+
spec.executables << "htpasswd-ruby"
|
31
|
+
spec.files = spec.test_files + spec.extra_rdoc_files +
|
32
|
+
FileList["lib/**/*.rb"]
|
33
|
+
|
34
|
+
spec.add_dependency("highline", ">= 1.4.0")
|
35
|
+
|
36
|
+
spec.platform = Gem::Platform::RUBY
|
37
|
+
|
38
|
+
spec.remote_user = "jjh"
|
39
|
+
spec.local_rdoc_dir = "doc/rdoc"
|
40
|
+
spec.remote_rdoc_dir = ""
|
41
|
+
spec.local_coverage_dir = "doc/coverage"
|
42
|
+
|
43
|
+
spec.remote_site_dir = "#{spec.name}/"
|
44
|
+
|
45
|
+
spec.post_install_message = <<EOM
|
46
|
+
Try out 'htpasswd-ruby' or 'htdigest-ruby' to get started.
|
47
|
+
EOM
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|