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/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
+