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