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/lib/htauth/md5.rb ADDED
@@ -0,0 +1,82 @@
1
+ require 'htauth/algorithm'
2
+ require 'digest/md5'
3
+
4
+ module HTAuth
5
+
6
+ # an implementation of the MD5 based encoding algorithm
7
+ # as used in the apache htpasswd -m option
8
+ class Md5 < Algorithm
9
+
10
+ DIGEST_LENGTH = 16
11
+
12
+ def initialize(params = {})
13
+ @salt = params['salt'] || params[:salt] || gen_salt
14
+ end
15
+
16
+ def prefix
17
+ "$apr1$"
18
+ end
19
+
20
+ # this algorigthm pulled straight from apr_md5_encode() and converted to ruby syntax
21
+ def encode(password)
22
+ primary = ::Digest::MD5.new
23
+ primary << password
24
+ primary << prefix
25
+ primary << @salt
26
+
27
+ md5_t = ::Digest::MD5.digest("#{password}#{@salt}#{password}")
28
+
29
+ l = password.length
30
+ while l > 0 do
31
+ slice_size = ( l > DIGEST_LENGTH ) ? DIGEST_LENGTH : l
32
+ primary << md5_t[0, slice_size]
33
+ l -= DIGEST_LENGTH
34
+ end
35
+
36
+ # weirdness
37
+ l = password.length
38
+ while l != 0
39
+ case (l & 1)
40
+ when 1
41
+ primary << 0.chr
42
+ when 0
43
+ primary << password[0,1]
44
+ end
45
+ l >>= 1
46
+ end
47
+
48
+ pd = primary.digest
49
+
50
+ encoded_password = "#{prefix}#{@salt}$"
51
+
52
+ # apr_md5_encode has this comment about a 60Mhz Pentium above this loop.
53
+ 1000.times do |x|
54
+ ctx = ::Digest::MD5.new
55
+ ctx << (( ( x & 1 ) == 1 ) ? password : pd[0,DIGEST_LENGTH])
56
+ (ctx << @salt) unless ( x % 3 ) == 0
57
+ (ctx << password) unless ( x % 7 ) == 0
58
+ ctx << (( ( x & 1 ) == 0 ) ? password : pd[0,DIGEST_LENGTH])
59
+ pd = ctx.digest
60
+ end
61
+
62
+
63
+ l = (pd[ 0]<<16) | (pd[ 6]<<8) | pd[12]
64
+ encoded_password << to_64(l, 4)
65
+
66
+ l = (pd[ 1]<<16) | (pd[ 7]<<8) | pd[13]
67
+ encoded_password << to_64(l, 4)
68
+
69
+ l = (pd[ 2]<<16) | (pd[ 8]<<8) | pd[14]
70
+ encoded_password << to_64(l, 4)
71
+
72
+ l = (pd[ 3]<<16) | (pd[ 9]<<8) | pd[15]
73
+ encoded_password << to_64(l, 4)
74
+
75
+ l = (pd[ 4]<<16) | (pd[10]<<8) | pd[ 5]
76
+ encoded_password << to_64(l, 4)
77
+ encoded_password << to_64(pd[11],2)
78
+
79
+ return encoded_password
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,174 @@
1
+ require 'htauth/passwd_file'
2
+ require 'ostruct'
3
+ require 'optparse'
4
+
5
+ require 'rubygems'
6
+ require 'highline'
7
+
8
+ module HTAuth
9
+ class Passwd
10
+
11
+ MAX_PASSWD_LENGTH = 255
12
+
13
+ attr_accessor :passwd_file
14
+
15
+ def initialize
16
+ @passwd_file = nil
17
+ end
18
+
19
+ def options
20
+ if @options.nil? then
21
+ @options = ::OpenStruct.new
22
+ @options.batch_mode = false
23
+ @options.file_mode = File::ALTER
24
+ @options.passwdfile = nil
25
+ @options.algorithm = Algorithm::EXISTING
26
+ @options.send_to_stdout = false
27
+ @options.show_version = false
28
+ @options.show_help = false
29
+ @options.username = nil
30
+ @options.delete_entry = false
31
+ @options.password = ""
32
+ end
33
+ @options
34
+ end
35
+
36
+ def option_parser
37
+ if not @option_parser then
38
+ @option_parser = OptionParser.new do |op|
39
+ op.banner = <<EOB
40
+ Usage:
41
+ #{op.program_name} [-cmdpsD] passwordfile username
42
+ #{op.program_name} -b[cmdpsD] passwordfile username password
43
+
44
+ #{op.program_name} -n[mdps] username
45
+ #{op.program_name} -nb[mdps] username password
46
+ EOB
47
+
48
+ op.separator ""
49
+
50
+ op.on("-b", "--batch", "Batch mode, get the password from the command line, rather than prompt") do |b|
51
+ options.batch_mode = b
52
+ end
53
+
54
+ op.on("-c", "--create", "Create a new file; this overwrites an existing file.") do |c|
55
+ options.file_mode = HTAuth::File::CREATE
56
+ end
57
+
58
+ op.on("-d", "--crypt", "Force CRYPT encryption of the password (default).") do |c|
59
+ options.algorithm = "crypt"
60
+ end
61
+
62
+ op.on("-D", "--delete", "Delete the specified user.") do |d|
63
+ options.delete_entry = d
64
+ end
65
+
66
+ op.on("-h", "--help", "Display this help.") do |h|
67
+ options.show_help = h
68
+ end
69
+
70
+ op.on("-m", "--md5", "Force MD5 encryption of the password (default on Windows).") do |m|
71
+ options.algorithm = "md5"
72
+ end
73
+
74
+ op.on("-n", "--stdout", "Do not update the file; Display the results on stdout instead.") do |n|
75
+ options.send_to_stdout = true
76
+ options.passwdfile = HTAuth::File::STDOUT_FLAG
77
+ end
78
+
79
+ op.on("-p", "--plaintext", "Do not encrypt the password (plaintext).") do |p|
80
+ options.algorithm = "plaintext"
81
+ end
82
+
83
+ op.on("-s", "--sha1", "Force SHA encryption of the password.") do |s|
84
+ options.algorithm = "sha1"
85
+ end
86
+
87
+ op.on("-v", "--version", "Show version info.") do |v|
88
+ options.show_version = v
89
+ end
90
+ end
91
+ end
92
+ @option_parser
93
+ end
94
+
95
+ def show_help
96
+ $stdout.puts option_parser
97
+ exit 1
98
+ end
99
+
100
+ def show_version
101
+ $stdout.puts "#{option_parser.program_name}: version #{HTAuth::VERSION}"
102
+ exit 1
103
+ end
104
+
105
+ def parse_options(argv)
106
+ begin
107
+ option_parser.parse!(argv)
108
+ show_version if options.show_version
109
+ show_help if options.show_help
110
+
111
+ raise ::OptionParser::ParseError, "Unable to send to stdout AND create a new file" if options.send_to_stdout and (options.file_mode == File::CREATE)
112
+ raise ::OptionParser::ParseError, "a username is needed" if options.send_to_stdout and argv.size < 1
113
+ raise ::OptionParser::ParseError, "a username and password are needed" if options.send_to_stdout and options.batch_mode and ( argv.size < 2 )
114
+ raise ::OptionParser::ParseError, "a passwordfile, username and password are needed " if not options.send_to_stdout and options.batch_mode and ( argv.size < 3 )
115
+ raise ::OptionParser::ParseError, "a passwordfile and username are needed" if argv.size < 2
116
+
117
+ options.passwdfile = argv.shift unless options.send_to_stdout
118
+ options.username = argv.shift
119
+ options.password = argv.shift if options.batch_mode
120
+
121
+ rescue ::OptionParser::ParseError => pe
122
+ $stderr.puts "ERROR: #{option_parser.program_name} - #{pe}"
123
+ show_help
124
+ exit 1
125
+ end
126
+ end
127
+
128
+ def run(argv)
129
+ begin
130
+ parse_options(argv)
131
+ passwd_file = PasswdFile.new(options.passwdfile, options.file_mode)
132
+
133
+ if options.delete_entry then
134
+ passwd_file.delete(options.username)
135
+ else
136
+ unless options.batch_mode
137
+ # initialize here so that if $stdin is overwritten it gest picked up
138
+ hl = ::HighLine.new
139
+
140
+ action = passwd_file.has_entry?(options.username) ? "Changing" : "Adding"
141
+
142
+ $stdout.puts "#{action} password for #{options.username}."
143
+
144
+ pw_in = hl.ask(" New password: ") { |q| q.echo = '*' }
145
+ raise PasswordError, "password '#{pw_in}' too long" if pw_in.length >= MAX_PASSWD_LENGTH
146
+
147
+ pw_validate = hl.ask("Re-type new password: ") { |q| q.echo = '*' }
148
+ raise PasswordError, "They don't match, sorry." unless pw_in == pw_validate
149
+ options.password = pw_in
150
+ end
151
+ passwd_file.add_or_update(options.username, options.password, options.algorithm)
152
+ end
153
+
154
+ passwd_file.save!
155
+
156
+ rescue HTAuth::FileAccessError => fae
157
+ msg = "Password file failure (#{options.passwdfile}) "
158
+ $stderr.puts "#{msg}: #{fae.message}"
159
+ exit 1
160
+ rescue HTAuth::PasswordError => pe
161
+ $stderr.puts "#{pe.message}"
162
+ exit 1
163
+ rescue HTAuth::PasswdFileError => fe
164
+ $stderr.puts "#{fe.message}"
165
+ exit 1
166
+ rescue SignalException => se
167
+ $stderr.puts
168
+ $stderr.puts "Interrupted"
169
+ exit 1
170
+ end
171
+ exit 0
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,97 @@
1
+
2
+ require 'htauth/entry'
3
+
4
+ module HTAuth
5
+ class InvalidPasswdEntry < StandardError ; end
6
+
7
+ # A single record in an htdigest file.
8
+ class PasswdEntry < Entry
9
+
10
+ attr_accessor :user
11
+ attr_accessor :digest
12
+ attr_reader :algorithm
13
+
14
+ class << self
15
+ def from_line(line)
16
+ parts = is_entry!(line)
17
+ d = PasswdEntry.new(parts[0])
18
+ d.digest = parts[1]
19
+ d.algorithm = Algorithm.algorithms_from_field(parts[1])
20
+ return d
21
+ end
22
+
23
+ # test if a line is an entry, raise InvalidPasswdEntry if it is not.
24
+ # an entry must be composed of 2 parts, username:encrypted_password
25
+ # where username, and password do not contain the ':' character
26
+ def is_entry!(line)
27
+ raise InvalidPasswdEntry, "line commented out" if line =~ /\A#/
28
+ parts = line.strip.split(":")
29
+ raise InvalidPasswdEntry, "line must be of the format username:pssword" if parts.size != 2
30
+ return parts
31
+ end
32
+
33
+ # test if a line is an entry and return true or false
34
+ def is_entry?(line)
35
+ begin
36
+ is_entry!(line)
37
+ return true
38
+ rescue InvalidPasswdEntry
39
+ return false
40
+ end
41
+ end
42
+ end
43
+
44
+ def initialize(user, password = "", alg = Algorithm::DEFAULT, alg_params = {} )
45
+ @user = user
46
+ alg = Algorithm::DEFAULT if alg == Algorithm::EXISTING
47
+ @algorithm = Algorithm.algorithm_from_name(alg, alg_params)
48
+ @digest = algorithm.encode(password)
49
+ end
50
+
51
+ def algorithm=(alg)
52
+ if alg.kind_of?(Array) then
53
+ if alg.size == 1 then
54
+ @algorithm = alg.first
55
+ else
56
+ @algorithm = alg
57
+ end
58
+ else
59
+ @algorithm = Algorithm.algorithm_from_name(alg) unless Algorithm::EXISTING == alg
60
+ end
61
+ return @algorithm
62
+ end
63
+
64
+ def password=(new_password)
65
+ if algorithm.kind_of?(Array) then
66
+ @algorithm = Algorithm.algorithm_from_name("crypt")
67
+ end
68
+ @digest = algorithm.encode(new_password)
69
+ end
70
+
71
+ # check the password and make sure it works, in the case that the algorithm is unknown it
72
+ # tries all of the ones that it thinks it could be, and marks the algorithm if it matches
73
+ def authenticated?(check_password)
74
+ authed = false
75
+ if algorithm.kind_of?(Array) then
76
+ algorithm.each do |alg|
77
+ if alg.encode(check_password) == digest then
78
+ @algorithm = alg
79
+ authed = true
80
+ break
81
+ end
82
+ end
83
+ else
84
+ authed = digest == algorithm.encode(check_password)
85
+ end
86
+ return authed
87
+ end
88
+
89
+ def key
90
+ return "#{user}"
91
+ end
92
+
93
+ def to_s
94
+ "#{user}:#{digest}"
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,86 @@
1
+ require 'stringio'
2
+ require 'tempfile'
3
+
4
+ require 'htauth/passwd_entry'
5
+
6
+ module HTAuth
7
+ class PasswdFileError < StandardError ; end
8
+
9
+ # PasswdFile provides API style access to an +htpasswd+ produced file
10
+ class PasswdFile < HTAuth::File
11
+
12
+ ENTRY_KLASS = HTAuth::PasswdEntry
13
+
14
+ # does the entry the the specified username and realm exist in the file
15
+ def has_entry?(username)
16
+ test_entry = PasswdEntry.new(username)
17
+ @entries.has_key?(test_entry.key)
18
+ end
19
+
20
+ # remove an entry from the file
21
+ def delete(username)
22
+ if has_entry?(username) then
23
+ ir = internal_record(username)
24
+ line_index = ir['line_index']
25
+ @entries.delete(ir['entry'].key)
26
+ @lines[line_index] = nil
27
+ dirty!
28
+ end
29
+ nil
30
+ end
31
+
32
+ # add or update an entry as appropriate
33
+ def add_or_update(username, password, algorithm = Algorithm::DEFAULT)
34
+ if has_entry?(username) then
35
+ update(username, password, algorithm)
36
+ else
37
+ add(username, password, algorithm)
38
+ end
39
+ end
40
+
41
+ # add an new record. raises an error if the entry exists.
42
+ def add(username, password, algorithm = Algorithm::DEFAULT)
43
+ raise PasswdFileError, "Unable to add already existing user #{username}" if has_entry?(username)
44
+ new_entry = PasswdEntry.new(username, password, algorithm)
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, password, algorithm = Algorithm::EXISTING)
54
+ raise PasswdFileError, "Unable to update non-existent user #{username}" unless has_entry?(username)
55
+ ir = internal_record(username)
56
+ ir['entry'].algorithm = algorithm
57
+ ir['entry'].password = password
58
+ @lines[ir['line_index']] = ir['entry'].to_s
59
+ dirty!
60
+ return nil
61
+ end
62
+
63
+ # fetches a copy of an entry from the file. Updateing the entry returned from fetch will NOT
64
+ # propogate back to the file.
65
+ def fetch(username)
66
+ return nil unless has_entry?(username)
67
+ ir = internal_record(username)
68
+ return ir['entry'].dup
69
+ end
70
+
71
+ def entry_klass
72
+ ENTRY_KLASS
73
+ end
74
+
75
+ def file_type
76
+ "passwd"
77
+ end
78
+
79
+ private
80
+
81
+ def internal_record(username)
82
+ e = PasswdEntry.new(username)
83
+ @entries[e.key]
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,18 @@
1
+ require 'htauth/algorithm'
2
+
3
+ module HTAuth
4
+
5
+ # the plaintext algorithm, which does absolutly nothing
6
+ class Plaintext < Algorithm
7
+ # ignore parameters
8
+ def initialize(params = {})
9
+ end
10
+ def prefix
11
+ ""
12
+ end
13
+
14
+ def encode(password)
15
+ "#{password}"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ require 'htauth/algorithm'
2
+ require 'digest/sha1'
3
+ require 'base64'
4
+
5
+ module HTAuth
6
+
7
+ # an implementation of the SHA based encoding algorithm
8
+ # as used in the apache htpasswd -s option
9
+ class Sha1 < Algorithm
10
+
11
+ # ignore the params
12
+ def initialize(params = {})
13
+ end
14
+
15
+ def prefix
16
+ "{SHA}"
17
+ end
18
+
19
+ def encode(password)
20
+ "#{prefix}#{Base64.encode64(::Digest::SHA1.digest(password)).strip}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,128 @@
1
+ require 'rubygems'
2
+ require 'rubygems/specification'
3
+ require 'rake'
4
+
5
+ module HTAuth
6
+ # Add some additional items to Gem::Specification
7
+ # A HTAuth::Specification adds additional pieces of information the
8
+ # typical gem specification
9
+ class Specification
10
+
11
+ RUBYFORGE_ROOT = "/var/www/gforge-projects/"
12
+
13
+ # user that accesses remote site
14
+ attr_accessor :remote_user
15
+
16
+ # remote host, default 'rubyforge.org'
17
+ attr_accessor :remote_host
18
+
19
+ # name the rdoc main
20
+ attr_accessor :rdoc_main
21
+
22
+ # local directory in development holding the generated rdoc
23
+ # default 'doc'
24
+ attr_accessor :local_rdoc_dir
25
+
26
+ # remote directory for storing rdoc, default 'doc'
27
+ attr_accessor :remote_rdoc_dir
28
+
29
+ # local directory for coverage report
30
+ attr_accessor :local_coverage_dir
31
+
32
+ # remote directory for storing coverage reports
33
+ # This defaults to 'coverage'
34
+ attr_accessor :remote_coverage_dir
35
+
36
+ # local directory for generated website, default +site/public+
37
+ attr_accessor :local_site_dir
38
+
39
+ # remote directory relative to +remote_root+ for the website.
40
+ # website.
41
+ attr_accessor :remote_site_dir
42
+
43
+ # is a .tgz to be created?, default 'true'
44
+ attr_accessor :need_tar
45
+
46
+ # is a .zip to be created, default 'true'
47
+ attr_accessor :need_zip
48
+
49
+
50
+ def initialize
51
+ @remote_user = nil
52
+ @remote_host = "rubyforge.org"
53
+
54
+ @rdoc_main = "README"
55
+ @local_rdoc_dir = "doc"
56
+ @remote_rdoc_dir = "doc"
57
+ @local_coverage_dir = "coverage"
58
+ @remote_coverage_dir = "coverage"
59
+ @local_site_dir = "site/public"
60
+ @remote_site_dir = "."
61
+
62
+ @need_tar = true
63
+ @need_zip = true
64
+
65
+ @spec = Gem::Specification.new
66
+
67
+ yield self if block_given?
68
+
69
+ # update rdoc options to take care of the rdoc_main if it is
70
+ # there, and add a default title if one is not given
71
+ if not @spec.rdoc_options.include?("--main") then
72
+ @spec.rdoc_options.concat(["--main", rdoc_main])
73
+ end
74
+
75
+ if not @spec.rdoc_options.include?("--title") then
76
+ @spec.rdoc_options.concat(["--title","'#{name} -- #{summary}'"])
77
+ end
78
+ end
79
+
80
+ # if this gets set then it overwrites what would be the
81
+ # rubyforge default. If rubyforge project is not set then use
82
+ # name. If rubyforge project and name are set, but they are
83
+ # different then assume that name is a subproject of the
84
+ # rubyforge project
85
+ def remote_root
86
+ if rubyforge_project.nil? or
87
+ rubyforge_project == name then
88
+ return RUBYFORGE_ROOT + "#{name}/"
89
+ else
90
+ return RUBYFORGE_ROOT + "#{rubyforge_project}/#{name}/"
91
+ end
92
+ end
93
+
94
+ # rdoc files is the same as what would be generated during gem
95
+ # installation. That is, everything in the require paths plus
96
+ # the rdoc_extra_files
97
+ #
98
+ def rdoc_files
99
+ flist = extra_rdoc_files.dup
100
+ @spec.require_paths.each do |rp|
101
+ flist << FileList["#{rp}/**/*.rb"]
102
+ end
103
+ flist.flatten.uniq
104
+ end
105
+
106
+ # calculate the remote directories
107
+ def remote_root_location
108
+ "#{remote_user}@#{remote_host}:#{remote_root}"
109
+ end
110
+
111
+ def remote_rdoc_location
112
+ remote_root_location + @remote_rdoc_dir
113
+ end
114
+
115
+ def remote_coverage_location
116
+ remote_root_loation + @remote_coverage_dir
117
+ end
118
+
119
+ def remote_site_location
120
+ remote_root_location + @remote_site_dir
121
+ end
122
+
123
+ # we delegate any other calls to spec
124
+ def method_missing(method_id,*params,&block)
125
+ @spec.send method_id, *params, &block
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,18 @@
1
+ module HTAuth
2
+ class Version
3
+ MAJOR = 1
4
+ MINOR = 0
5
+ BUILD = 0
6
+
7
+ class << self
8
+ def to_a
9
+ [MAJOR, MINOR, BUILD]
10
+ end
11
+
12
+ def to_s
13
+ to_a.join(".")
14
+ end
15
+ end
16
+ end
17
+ VERSION = Version.to_s
18
+ end
data/lib/htauth.rb ADDED
@@ -0,0 +1,27 @@
1
+ module HTAuth
2
+
3
+ ROOT_DIR = ::File.expand_path(::File.join(::File.dirname(__FILE__),".."))
4
+ LIB_DIR = ::File.join(ROOT_DIR,"lib").freeze
5
+
6
+ #
7
+ # Utility method to require all files ending in .rb in the directory
8
+ # with the same name as this file minus .rb
9
+ #
10
+ def require_all_libs_relative_to(fname)
11
+ prepend = ::File.basename(fname,".rb")
12
+ search_me = ::File.join(::File.dirname(fname),prepend)
13
+
14
+ Dir.entries(search_me).each do |rb|
15
+ if ::File.extname(rb) == ".rb" then
16
+ require "#{prepend}/#{::File.basename(rb,".rb")}"
17
+ end
18
+ end
19
+ end
20
+ module_function :require_all_libs_relative_to
21
+
22
+ class FileAccessError < StandardError ; end
23
+ class TempFileError < StandardError ; end
24
+ class PasswordError < StandardError ; end
25
+ end
26
+
27
+ HTAuth.require_all_libs_relative_to(__FILE__)
@@ -0,0 +1,18 @@
1
+
2
+ require File.join(File.dirname(__FILE__),"spec_helper.rb")
3
+
4
+ require 'htauth/crypt'
5
+
6
+ describe HTAuth::Crypt do
7
+ it "has a prefix" do
8
+ HTAuth::Crypt.new.prefix.should == ""
9
+ end
10
+
11
+ it "encrypts the same way that apache does" do
12
+ apache_salt = "L0LDd/.."
13
+ apache_result = "L0ekWYm59LT1M"
14
+ crypt = HTAuth::Crypt.new({ :salt => apache_salt} )
15
+ crypt.encode("a secret").should == apache_result
16
+ end
17
+ end
18
+