htauth 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES ADDED
@@ -0,0 +1,4 @@
1
+ = rpasswd Changelog
2
+ === Version 1.0.0
3
+
4
+ * Initial public release
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
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'htauth'
5
+ rescue LoadError
6
+ path = File.expand_path(File.join(File.dirname(__FILE__),"..","lib"))
7
+ raise if $:.include?(path)
8
+ $: << path
9
+ retry
10
+ end
11
+
12
+ HTAuth::Digest.new.run(ARGV)
data/bin/htpasswd-ruby ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'htauth'
5
+ rescue LoadError
6
+ path = File.expand_path(File.join(File.dirname(__FILE__),"..","lib"))
7
+ raise if $:.include?(path)
8
+ $: << path
9
+ retry
10
+ end
11
+
12
+ HTAuth::Passwd.new.run(ARGV)
@@ -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'
@@ -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
@@ -0,0 +1,9 @@
1
+ module HTAuth
2
+
3
+ # base class from which all entries are derived
4
+ class Entry
5
+ def dup
6
+ self.class.from_line(self.to_s)
7
+ end
8
+ end
9
+ end
@@ -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
+