htauth 1.0.0

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