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/{CHANGES → HISTORY}
RENAMED
data/README
CHANGED
@@ -2,17 +2,18 @@
|
|
2
2
|
|
3
3
|
* Homepage[http://copiousfreetime.rubyforge.org/htauth]
|
4
4
|
* {Rubyforge Project}[http://rubyforge.org/projects/copiousfreetime/]
|
5
|
-
*
|
5
|
+
* Github[http://github.com/copiousfreetime/htauth/tree/master]
|
6
|
+
* email jeremy at copiousfreetime dot org
|
6
7
|
|
7
8
|
== DESCRIPTION
|
8
9
|
|
9
|
-
HTAuth is a pure ruby replacement for the Apache support programs
|
10
|
-
|
10
|
+
HTAuth is a pure ruby replacement for the Apache support programs htdigest and
|
11
|
+
htpasswd. Command line and API access are provided for access to htdigest and
|
11
12
|
htpasswd files.
|
12
13
|
|
13
14
|
== FEATURES
|
14
15
|
|
15
|
-
|
16
|
+
HTAuth provides to drop in commands *htdigest-ruby* and *htpasswd-ruby* that
|
16
17
|
can manipulate the digest and passwd files in the same manner as Apache's
|
17
18
|
original commands.
|
18
19
|
|
data/bin/htdigest-ruby
CHANGED
@@ -1,12 +1,19 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
begin
|
4
|
+
require 'highline'
|
5
|
+
rescue LoadError
|
6
|
+
require 'rubygems'
|
7
|
+
require 'highline'
|
8
|
+
end
|
9
|
+
|
3
10
|
begin
|
4
|
-
|
11
|
+
require 'htauth'
|
5
12
|
rescue LoadError
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
13
|
+
path = File.expand_path(File.join(File.dirname(__FILE__),"..","lib"))
|
14
|
+
raise if $:.include?(path)
|
15
|
+
$: << path
|
16
|
+
retry
|
10
17
|
end
|
11
18
|
|
12
|
-
HTAuth::Digest.new.run(ARGV)
|
19
|
+
HTAuth::Digest.new.run(ARGV, ENV)
|
data/bin/htpasswd-ruby
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
begin
|
4
|
+
require 'highline'
|
5
|
+
rescue LoadError
|
6
|
+
require 'rubygems'
|
7
|
+
require 'highline'
|
8
|
+
end
|
9
|
+
|
3
10
|
begin
|
4
11
|
require 'htauth'
|
5
12
|
rescue LoadError
|
@@ -9,4 +16,4 @@ rescue LoadError
|
|
9
16
|
retry
|
10
17
|
end
|
11
18
|
|
12
|
-
HTAuth::Passwd.new.run(ARGV)
|
19
|
+
HTAuth::Passwd.new.run(ARGV, ENV)
|
data/gemspec.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'htauth/version'
|
3
|
+
require 'tasks/config'
|
4
|
+
|
5
|
+
HTAuth::GEM_SPEC = Gem::Specification.new do |spec|
|
6
|
+
proj = Configuration.for('project')
|
7
|
+
spec.name = proj.name
|
8
|
+
spec.version = HTAuth::VERSION
|
9
|
+
|
10
|
+
spec.author = proj.author
|
11
|
+
spec.email = proj.email
|
12
|
+
spec.homepage = proj.homepage
|
13
|
+
spec.summary = proj.summary
|
14
|
+
spec.description = proj.description
|
15
|
+
spec.platform = Gem::Platform::RUBY
|
16
|
+
|
17
|
+
|
18
|
+
pkg = Configuration.for('packaging')
|
19
|
+
spec.files = pkg.files.all
|
20
|
+
spec.executables = pkg.files.bin.collect { |b| File.basename(b) }
|
21
|
+
|
22
|
+
# add dependencies here
|
23
|
+
# spec.add_dependency("rake", ">= 0.8.1")
|
24
|
+
spec.add_dependency("highline", "~> 1.4.0")
|
25
|
+
|
26
|
+
|
27
|
+
if rdoc = Configuration.for_if_exist?('rdoc') then
|
28
|
+
spec.has_rdoc = true
|
29
|
+
spec.extra_rdoc_files = pkg.files.rdoc
|
30
|
+
spec.rdoc_options = rdoc.options + [ "--main" , rdoc.main_page ]
|
31
|
+
else
|
32
|
+
spec.has_rdoc = false
|
33
|
+
end
|
34
|
+
|
35
|
+
if test = Configuration.for_if_exist?('testing') then
|
36
|
+
spec.test_files = test.files
|
37
|
+
end
|
38
|
+
|
39
|
+
if rf = Configuration.for_if_exist?('rubyforge') then
|
40
|
+
spec.rubyforge_project = rf.project
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/lib/htauth.rb
CHANGED
@@ -1,14 +1,36 @@
|
|
1
|
+
#--
|
2
|
+
# Copyrigth (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details
|
4
|
+
#++
|
5
|
+
|
1
6
|
module HTAuth
|
2
|
-
|
3
|
-
ROOT_DIR = ::File.expand_path(::File.join(::File.dirname(__FILE__),".."))
|
4
|
-
LIB_DIR = ::File.join(ROOT_DIR,"lib").freeze
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
# The root directory of the project is considered to be the parent directory
|
9
|
+
# of the 'lib' directory.
|
10
|
+
#
|
11
|
+
def self.root_dir
|
12
|
+
unless @root_dir
|
13
|
+
path_parts = ::File.expand_path( __FILE__ ).split( ::File::SEPARATOR )
|
14
|
+
lib_index = path_parts.rindex( 'lib' )
|
15
|
+
@root_dir = path_parts[ 0...lib_index].join( ::File::SEPARATOR ) + ::File::SEPARATOR
|
16
|
+
end
|
17
|
+
return @root_dir
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.lib_path( *args )
|
21
|
+
self.sub_path( "lib", *args )
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.sub_path( sub, *args )
|
25
|
+
sp = ::File.join( root_dir, sub ) + ::File::SEPARATOR
|
26
|
+
sp = ::File.join( sp, *args ) if args
|
27
|
+
end
|
28
|
+
|
29
|
+
class FileAccessError < StandardError ; end
|
30
|
+
class TempFileError < StandardError ; end
|
31
|
+
class PasswordError < StandardError ; end
|
9
32
|
end
|
10
33
|
|
11
34
|
require 'htauth/version'
|
12
|
-
require 'htauth/gemspec'
|
13
35
|
require 'htauth/passwd'
|
14
36
|
require 'htauth/digest'
|
data/lib/htauth/algorithm.rb
CHANGED
@@ -1,67 +1,68 @@
|
|
1
1
|
require 'htauth'
|
2
2
|
|
3
3
|
module HTAuth
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
class InvalidAlgorithmError < StandardError ; end
|
5
|
+
# base class all the Passwd algorithms derive from
|
6
|
+
class Algorithm
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
SALT_CHARS = (%w[ . / ] + ("0".."9").to_a + ('A'..'Z').to_a + ('a'..'z').to_a).freeze
|
9
|
+
DEFAULT = ( RUBY_PLATFORM !~ /mswin32/ ) ? "crypt" : "md5"
|
10
|
+
EXISTING = "existing"
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
class << self
|
13
|
+
def algorithm_from_name(a_name, params = {})
|
14
|
+
raise InvalidAlgorithmError, "`#{a_name}' is an invalid encryption algorithm, use one of #{sub_klasses.keys.join(', ')}" unless sub_klasses[a_name.downcase]
|
15
|
+
sub_klasses[a_name.downcase].new(params)
|
16
|
+
end
|
17
17
|
|
18
|
-
|
19
|
-
|
18
|
+
def algorithms_from_field(password_field)
|
19
|
+
matches = []
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
21
|
+
if password_field.index(sub_klasses['sha1'].new.prefix) then
|
22
|
+
matches << sub_klasses['sha1'].new
|
23
|
+
elsif password_field.index(sub_klasses['md5'].new.prefix) then
|
24
|
+
p = password_field.split("$")
|
25
|
+
matches << sub_klasses['md5'].new( :salt => p[2] )
|
26
|
+
else
|
27
|
+
matches << sub_klasses['plaintext'].new
|
28
|
+
matches << sub_klasses['crypt'].new( :salt => password_field[0,2] )
|
29
|
+
end
|
30
30
|
|
31
|
-
|
32
|
-
|
31
|
+
return matches
|
32
|
+
end
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
34
|
+
def inherited(sub_klass)
|
35
|
+
k = sub_klass.name.split("::").last.downcase
|
36
|
+
sub_klasses[k] = sub_klass
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
39
|
+
def sub_klasses
|
40
|
+
@sub_klasses ||= {}
|
41
|
+
end
|
42
|
+
end
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
# 8 bytes of random items from SALT_CHARS
|
48
|
-
def gen_salt
|
49
|
-
chars = []
|
50
|
-
8.times { chars << SALT_CHARS[rand(SALT_CHARS.size)] }
|
51
|
-
chars.join('')
|
52
|
-
end
|
44
|
+
def prefix ; end
|
45
|
+
def encode(password) ; end
|
53
46
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
47
|
+
# 8 bytes of random items from SALT_CHARS
|
48
|
+
def gen_salt
|
49
|
+
chars = []
|
50
|
+
8.times { chars << SALT_CHARS[rand(SALT_CHARS.size)] }
|
51
|
+
chars.join('')
|
52
|
+
end
|
53
|
+
|
54
|
+
# this is not the Base64 encoding, this is the to64() method from apr
|
55
|
+
def to_64(number, rounds)
|
56
|
+
r = StringIO.new
|
57
|
+
rounds.times do |x|
|
58
|
+
r.print(SALT_CHARS[number % 64])
|
59
|
+
number >>= 6
|
60
|
+
end
|
61
|
+
return r.string
|
63
62
|
end
|
63
|
+
end
|
64
64
|
end
|
65
|
+
|
65
66
|
require 'htauth/md5'
|
66
67
|
require 'htauth/sha1'
|
67
68
|
require 'htauth/crypt'
|
data/lib/htauth/crypt.rb
CHANGED
@@ -2,19 +2,19 @@ require 'htauth/algorithm'
|
|
2
2
|
|
3
3
|
module HTAuth
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(params = {})
|
9
|
-
@salt = params[:salt] || params['salt'] || gen_salt
|
10
|
-
end
|
5
|
+
# The basic crypt algorithm
|
6
|
+
class Crypt < Algorithm
|
11
7
|
|
12
|
-
|
8
|
+
def initialize(params = {})
|
9
|
+
@salt = params[:salt] || params['salt'] || gen_salt
|
10
|
+
end
|
11
|
+
|
12
|
+
def prefix
|
13
13
|
""
|
14
|
-
|
14
|
+
end
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
end
|
16
|
+
def encode(password)
|
17
|
+
password.crypt(@salt)
|
19
18
|
end
|
19
|
+
end
|
20
20
|
end
|
data/lib/htauth/digest.rb
CHANGED
@@ -4,127 +4,126 @@ require 'htauth/digest_file'
|
|
4
4
|
require 'ostruct'
|
5
5
|
require 'optparse'
|
6
6
|
|
7
|
-
require 'rubygems'
|
8
7
|
require 'highline'
|
9
8
|
|
10
9
|
module HTAuth
|
11
|
-
|
10
|
+
class Digest
|
12
11
|
|
13
|
-
|
12
|
+
MAX_PASSWD_LENGTH = 255
|
14
13
|
|
15
|
-
|
14
|
+
attr_accessor :digest_file
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
def initialize
|
17
|
+
@digest_file = nil
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
20
|
+
def options
|
21
|
+
if @options.nil? then
|
22
|
+
@options = ::OpenStruct.new
|
23
|
+
@options.show_version = false
|
24
|
+
@options.show_help = false
|
25
|
+
@options.file_mode = DigestFile::ALTER
|
26
|
+
@options.passwdfile = nil
|
27
|
+
@options.realm = nil
|
28
|
+
@options.username = nil
|
29
|
+
@options.delete_entry = false
|
30
|
+
end
|
31
|
+
@options
|
32
|
+
end
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
55
|
-
end
|
56
|
-
@option_parser
|
34
|
+
def option_parser
|
35
|
+
if not @option_parser then
|
36
|
+
@option_parser = OptionParser.new do |op|
|
37
|
+
op.banner = "Usage: #{op.program_name} [options] passwordfile realm username"
|
38
|
+
op.on("-c", "--create", "Create a new digest password file; this overwrites an existing file.") do |c|
|
39
|
+
options.file_mode = DigestFile::CREATE
|
40
|
+
end
|
41
|
+
|
42
|
+
op.on("-D", "--delete", "Delete the specified user.") do |d|
|
43
|
+
options.delete_entry = d
|
44
|
+
end
|
45
|
+
|
46
|
+
op.on("-h", "--help", "Display this help.") do |h|
|
47
|
+
options.show_help = h
|
48
|
+
end
|
49
|
+
|
50
|
+
op.on("-v", "--version", "Show version info.") do |v|
|
51
|
+
options.show_version = v
|
52
|
+
end
|
57
53
|
end
|
54
|
+
end
|
55
|
+
@option_parser
|
56
|
+
end
|
58
57
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
58
|
+
def show_help
|
59
|
+
$stdout.puts option_parser
|
60
|
+
exit 1
|
61
|
+
end
|
63
62
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
63
|
+
def show_version
|
64
|
+
$stdout.puts "#{option_parser.program_name}: version #{HTAuth::VERSION}"
|
65
|
+
exit 1
|
66
|
+
end
|
68
67
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
68
|
+
def parse_options(argv)
|
69
|
+
begin
|
70
|
+
option_parser.parse!(argv)
|
71
|
+
show_version if options.show_version
|
72
|
+
show_help if options.show_help or argv.size < 3
|
73
|
+
|
74
|
+
options.passwdfile = argv.shift
|
75
|
+
options.realm = argv.shift
|
76
|
+
options.username = argv.shift
|
77
|
+
rescue ::OptionParser::ParseError => pe
|
78
|
+
$stderr.puts "ERROR: #{option_parser.program_name} - #{pe}"
|
79
|
+
$stderr.puts "Try `#{option_parser.program_name} --help` for more information"
|
80
|
+
exit 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def run(argv, env = ENV)
|
85
|
+
begin
|
86
|
+
parse_options(argv)
|
87
|
+
digest_file = DigestFile.new(options.passwdfile, options.file_mode)
|
88
|
+
|
89
|
+
if options.delete_entry then
|
90
|
+
digest_file.delete(options.username, options.realm)
|
91
|
+
else
|
92
|
+
# initialize here so that if $stdin is overwritten it gets picked up
|
93
|
+
hl = ::HighLine.new
|
94
|
+
|
95
|
+
action = digest_file.has_entry?(options.username, options.realm) ? "Changing" : "Adding"
|
84
96
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
hl = ::HighLine.new
|
95
|
-
|
96
|
-
action = digest_file.has_entry?(options.username, options.realm) ? "Changing" : "Adding"
|
97
|
-
|
98
|
-
$stdout.puts "#{action} password for #{options.username} in realm #{options.realm}."
|
99
|
-
|
100
|
-
pw_in = hl.ask(" New password: ") { |q| q.echo = '*' }
|
101
|
-
raise PasswordError, "password '#{pw_in}' too long" if pw_in.length >= MAX_PASSWD_LENGTH
|
102
|
-
|
103
|
-
pw_validate = hl.ask("Re-type new password: ") { |q| q.echo = '*' }
|
104
|
-
raise PasswordError, "They don't match, sorry." unless pw_in == pw_validate
|
105
|
-
|
106
|
-
digest_file.add_or_update(options.username, options.realm, pw_in)
|
107
|
-
end
|
108
|
-
|
109
|
-
digest_file.save!
|
110
|
-
|
111
|
-
rescue HTAuth::FileAccessError => fae
|
112
|
-
msg = "Could not open password file #{options.passwdfile} "
|
113
|
-
$stderr.puts "#{msg}: #{fae.message}"
|
114
|
-
$stderr.puts fae.backtrace.join("\n")
|
115
|
-
exit 1
|
116
|
-
rescue HTAuth::PasswordError => pe
|
117
|
-
$stderr.puts "#{pe.message}"
|
118
|
-
exit 1
|
119
|
-
rescue HTAuth::DigestFileError => fe
|
120
|
-
$stderr.puts "#{fe.message}"
|
121
|
-
exit 1
|
122
|
-
rescue SignalException => se
|
123
|
-
$stderr.puts
|
124
|
-
$stderr.puts "Interrupted"
|
125
|
-
exit 1
|
126
|
-
end
|
127
|
-
exit 0
|
97
|
+
$stdout.puts "#{action} password for #{options.username} in realm #{options.realm}."
|
98
|
+
|
99
|
+
pw_in = hl.ask(" New password: ") { |q| q.echo = '*' }
|
100
|
+
raise PasswordError, "password '#{pw_in}' too long" if pw_in.length >= MAX_PASSWD_LENGTH
|
101
|
+
|
102
|
+
pw_validate = hl.ask("Re-type new password: ") { |q| q.echo = '*' }
|
103
|
+
raise PasswordError, "They don't match, sorry." unless pw_in == pw_validate
|
104
|
+
|
105
|
+
digest_file.add_or_update(options.username, options.realm, pw_in)
|
128
106
|
end
|
107
|
+
|
108
|
+
digest_file.save!
|
109
|
+
|
110
|
+
rescue HTAuth::FileAccessError => fae
|
111
|
+
msg = "Could not open password file #{options.passwdfile} "
|
112
|
+
$stderr.puts "#{msg}: #{fae.message}"
|
113
|
+
$stderr.puts fae.backtrace.join("\n")
|
114
|
+
exit 1
|
115
|
+
rescue HTAuth::PasswordError => pe
|
116
|
+
$stderr.puts "#{pe.message}"
|
117
|
+
exit 1
|
118
|
+
rescue HTAuth::DigestFileError => fe
|
119
|
+
$stderr.puts "#{fe.message}"
|
120
|
+
exit 1
|
121
|
+
rescue SignalException => se
|
122
|
+
$stderr.puts
|
123
|
+
$stderr.puts "Interrupted"
|
124
|
+
exit 1
|
125
|
+
end
|
126
|
+
exit 0
|
129
127
|
end
|
128
|
+
end
|
130
129
|
end
|