maiha-htpasswd 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,117 @@
1
+ Htpasswd
2
+ ========
3
+ This plugin allows controllers to use HTTP Basic and Digest access authentication.
4
+ You can specify user passwords like this.
5
+
6
+ * inline plain password
7
+ * inline crypted password
8
+ * external password file
9
+
10
+
11
+ Syntax
12
+ ======
13
+
14
+ htpasswd *options*
15
+ htdigest *options*
16
+
17
+
18
+ Options
19
+ =======
20
+
21
+ * user : user name # default: nil
22
+ * pass : password (format depends on :type) # default: nil
23
+ * type : one of ( :plain | :crypted ) # default: :plain
24
+ * file : external file path # default: nil
25
+ * realm : realm value # default: "Authorization"
26
+ * class : specify ActiveRecord class for account # default: nil
27
+ * scheme : auth scheme # default: automatically set by method name
28
+
29
+
30
+ Usage
31
+ =====
32
+
33
+ (1) Basic Access Authentication
34
+
35
+ class AdminController < ApplicationController
36
+ htpasswd :user=>"maiha", :pass=>"berryz"
37
+ htpasswd :user=>"maiha", :pass=>"7Et1Y7tCawx32", :type=>:crypted
38
+ htpasswd :user=>"maiha", :pass=>"berryz", :realm=>"Member Only"
39
+ htpasswd :file=>"/usr/local/apache/passwd/.htpasswd"
40
+ htpasswd :class=>"Account" # authorize user with Account#username and Account#password
41
+ htpasswd :class=>"Account", :user=>"login", :pass=>"secret" # use "login" and "secret" columns
42
+ end
43
+
44
+
45
+ (2) Digest Access Authentication
46
+
47
+ class AdminController < ApplicationController
48
+ htdigest :user=>"maiha", :pass=>"berryz"
49
+ htdigest :user=>"maiha", :pass=>"812b1d067e9ce1e44f09215339e3cd69", :type=>:crypted
50
+ htdigest :file=>"/usr/local/apache/passwd/.htdigest"
51
+ htdigest :class=>"Account" # Account#password should be realm-considered value.
52
+ end
53
+
54
+
55
+ (3) Multiple Access Authentications
56
+
57
+ class AdminController < ApplicationController
58
+ htpasswd :user=>"maiha", :pass=>"berryz"
59
+ htdigest :user=>"airi" , :pass=>"cute"
60
+ end
61
+
62
+ Although user 'maiha' is authorized by Basic auth,
63
+ user 'airi' is authorized by Digest auth in this case.
64
+ And this controller returns Digest one as a 401 response
65
+ because it is strongest auth-scheme in above schemes.
66
+
67
+
68
+ (4) Authorized User Name
69
+
70
+ class AdminController < ApplicationController
71
+ htpasswd :user=>"maiha", :pass=>"berryz"
72
+ def index
73
+ render :text=>"current_user: #{@htpasswd_authorized_username}"
74
+ end
75
+ end
76
+
77
+ Authorized user name is set in @htpasswd_authorized_username.
78
+
79
+
80
+ (0) Creating a htdigest file
81
+
82
+ >> Htpasswd::Auths::Digest.new(:user=>"maiha", :pass=>"berryz").entry
83
+ => "maiha:Authorization:812b1d067e9ce1e44f09215339e3cd69"
84
+
85
+ This acts same as following unix command.
86
+
87
+ % htdigest -c filename maiha
88
+
89
+
90
+ Restrictions
91
+ ============
92
+
93
+ * 'realm' value should not contain any commas and semicolons.
94
+
95
+
96
+ Rails
97
+ =====
98
+
99
+ 1.2 : OK
100
+ 2.1 : OK
101
+ 2.2 : OK
102
+
103
+
104
+ Test
105
+ ====
106
+
107
+ Just type.
108
+
109
+ % ruby vendor/plugins/htpasswd/test/htpasswd_test.rb
110
+ % ruby vendor/plugins/htpasswd/test/htdigest_test.rb
111
+
112
+
113
+ Author
114
+ ======
115
+ The original author is Kawamura.
116
+ Composed by maiha@wota.jp
117
+
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the htpasswd plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the htpasswd plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'Htpasswd'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
data/init.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'htpasswd/class_methods'
2
+
3
+ require 'htpasswd/acls/base'
4
+ require 'htpasswd/acls/plain'
5
+ require 'htpasswd/acls/crypted'
6
+ require 'htpasswd/acls/digest'
7
+ require 'htpasswd/acls/htpasswd'
8
+ require 'htpasswd/acls/htdigest'
9
+ require 'htpasswd/acls/active_record'
10
+
11
+ require 'htpasswd/auths/base'
12
+ require 'htpasswd/auths/basic'
13
+ require 'htpasswd/auths/digest'
14
+
15
+ ActionController::Base.send(:include, Htpasswd)
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,25 @@
1
+ module Htpasswd
2
+ module Acls
3
+ class ActiveRecord < Base
4
+ def authorize_user(scheme)
5
+ active_record.send("find_by_%s" % (@options[:user] || :username), scheme.user)
6
+ end
7
+
8
+ def authorize_pass(scheme)
9
+ obj = active_record.send("find_by_%s" % (@options[:user] || :username), scheme.user)
10
+ scheme.authorize_pass(obj[@options[:pass] || :password])
11
+ rescue
12
+ raise IncorrectPassword
13
+ end
14
+
15
+ protected
16
+ def active_record
17
+ @active_record ||= @options[:class].to_s.classify.constantize
18
+ rescue
19
+ raise ConfigurationError, "invalid active_record class name: '%s'" % @options[:class].to_s
20
+ end
21
+
22
+ register self
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,76 @@
1
+ module Htpasswd
2
+ module Acls
3
+ class Base
4
+ delegate :logger, :to=>"ActionController::Base"
5
+
6
+ @@entries = {}
7
+
8
+ class << self
9
+ def [](name)
10
+ name = name.to_s.classify
11
+ @@entries[name] or raise UnknownAccessControl, name
12
+ end
13
+
14
+ def register(klass)
15
+ @@entries[klass.name.demodulize.classify] = klass
16
+ end
17
+ end
18
+
19
+ def initialize(options)
20
+ @options = options
21
+ end
22
+
23
+ def user
24
+ @options[:user]
25
+ end
26
+
27
+ def pass
28
+ @options[:pass]
29
+ end
30
+
31
+ def type
32
+ @options[:type]
33
+ end
34
+
35
+ def authorized?(scheme)
36
+ authorize_type(scheme) && authorize_user(scheme) && authorize_pass(scheme) && scheme.user
37
+ end
38
+
39
+ def authorize_type(scheme)
40
+ scheme.authorize_type(type)
41
+ end
42
+
43
+ def authorize_user(scheme)
44
+ scheme.authorize_user(user)
45
+ end
46
+
47
+ def authorize_pass(scheme)
48
+ scheme.authorize_pass(pass)
49
+ end
50
+
51
+ register self
52
+ end
53
+
54
+ class CompositeBase < Base
55
+ delegate :each, :<<, :to=>"@entries"
56
+
57
+ def initialize(*args)
58
+ @entries = []
59
+ initialize_composite(*args)
60
+ end
61
+
62
+ def initialize_composite(*args)
63
+ raise NotImplementedError
64
+ end
65
+
66
+ def authorized?(scheme)
67
+ each do |entry|
68
+ if user = entry.authorized?(scheme)
69
+ return user
70
+ end
71
+ end
72
+ return false
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,11 @@
1
+ module Htpasswd
2
+ module Acls
3
+ class Crypted < Base
4
+ def authorize_pass(scheme)
5
+ pass == scheme.pass.crypt(pass) or
6
+ raise IncorrectPassword
7
+ end
8
+ register self
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module Htpasswd
2
+ module Acls
3
+ class Digest < Base
4
+ def authorize_type(scheme)
5
+ scheme.is_a?(Auths::Digest)
6
+ end
7
+ register self
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ module Htpasswd
2
+ module Acls
3
+ class Htdigest < CompositeBase
4
+ def initialize_composite(options)
5
+ IO.foreach(options[:file]) do |line|
6
+ user, realm, pass = line.chomp.split(':', 3)
7
+ self << Acls::Digest.new(:user=>user, :realm=>realm, :pass=>pass)
8
+ end
9
+ rescue => err
10
+ logger.debug("%s: [%s]%s" % [self.class, err.class, err])
11
+ raise ConfigurationError, "Cannot read password file. '#{options[:file]}'"
12
+ end
13
+
14
+ register self
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Htpasswd
2
+ module Acls
3
+ class Htpasswd < CompositeBase
4
+ def initialize_composite(options)
5
+ IO.foreach(options[:file]) do |line|
6
+ user, pass = line.chomp.split(':', 2)
7
+ self << Crypted.new(:type=>:crypted, :user=>user, :pass=>pass)
8
+ end
9
+ rescue => err
10
+ logger.debug("%s: [%s]%s" % [self.class, err.class, err])
11
+ raise ConfigurationError, "Cannot read password file. '#{options[:file]}'"
12
+ end
13
+
14
+ register self
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module Htpasswd
2
+ module Acls
3
+ class Plain < Base
4
+ def authorize_pass(scheme)
5
+ pass == scheme.pass or
6
+ raise IncorrectPassword
7
+ end
8
+ register self
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,108 @@
1
+ module Htpasswd
2
+ module Auths
3
+ @@schemas = {}
4
+
5
+ module_function
6
+ def [](auth_scheme)
7
+ @@schemas[auth_scheme.to_s.classify]
8
+ end
9
+
10
+ def scheme(controller)
11
+ case controller
12
+ when ActionController::Base
13
+ returning authorization = instantiate(extract_header(controller.request.env)) do
14
+ authorization.set_controller(controller)
15
+ end
16
+ else
17
+ instantiate(controller.to_s)
18
+ end
19
+ end
20
+
21
+ def instantiate(header)
22
+ raise HeaderNotFound if header.blank?
23
+ ActionController::Base.logger.debug "Htpasswd accepts authorization header: '#{header}'"
24
+ type, data = header.to_s.split(' ', 2)
25
+ klass = self[type] or raise UnknownSchemeError, type
26
+ klass.parse(data)
27
+ end
28
+
29
+ def extract_header(hash)
30
+ [
31
+ 'X-HTTP_AUTHORIZATION', # for Apache/mod_rewrite
32
+ 'REDIRECT_X_HTTP_AUTHORIZATION', # for Apache2/mod_rewrite
33
+ 'Authorization', # for Apace/mod_fastcgi with -pass-header Authorization
34
+ 'HTTP_AUTHORIZATION', # this is the regular location
35
+ ].map{|name| hash[name]}.compact.first
36
+ end
37
+
38
+ def default_realm
39
+ "Authorization"
40
+ end
41
+
42
+ class Base
43
+ delegate :logger, :to=>"ActionController::Base"
44
+
45
+ class << self
46
+ def parse(data)
47
+ raise NotImplementedError, "subclass responsibility"
48
+ end
49
+
50
+ def <=> (other)
51
+ strength <=> other.strength
52
+ end
53
+ end
54
+
55
+ attr_accessor :options
56
+ def initialize(options = {})
57
+ @options = options.with_indifferent_access
58
+ end
59
+
60
+ # return authorized username or raise exception
61
+ def authorize(entries)
62
+ entries.each do |entry|
63
+ if user = entry.authorized?(self)
64
+ return user
65
+ end
66
+ end
67
+ raise UnknownUserAccount
68
+ end
69
+
70
+ def authorize_type(type)
71
+ true
72
+ end
73
+
74
+ def authorize_user(user)
75
+ options[:user] == user
76
+ end
77
+
78
+ def authorize_pass(pass)
79
+ options[:pass] == pass or raise IncorrectPassword
80
+ end
81
+
82
+ def set_controller(controller)
83
+ # nop
84
+ end
85
+
86
+ def realm
87
+ options[:realm] || Auths.default_realm
88
+ end
89
+
90
+ def scheme
91
+ self.class.name.demodulize
92
+ end
93
+
94
+ def user
95
+ options[:user]
96
+ end
97
+
98
+ def pass
99
+ options[:pass]
100
+ end
101
+
102
+ def random(size = 16, array = nil)
103
+ array ||= (0..9).to_a + ('a'..'f').to_a
104
+ (0...size).map{array[rand(array.size)]}.join
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,21 @@
1
+ module Htpasswd
2
+ module Auths
3
+ class Basic < Base
4
+ class << self
5
+ def parse(data)
6
+ user, pass = Base64.decode64(data.to_s).split(':', 2)
7
+ new(:user=>user, :pass=>pass)
8
+ end
9
+
10
+ def strength
11
+ 10
12
+ end
13
+ end
14
+
15
+ def server_header
16
+ %Q|Basic realm="%s"| % realm
17
+ end
18
+ end
19
+ @@schemas["Basic"] = Basic
20
+ end
21
+ end
@@ -0,0 +1,152 @@
1
+ module Htpasswd
2
+ module Auths
3
+ class Digest < Base
4
+ class << self
5
+ PARAMETER_VALID_KEY_MAPPINGS = {
6
+ :username => :user,
7
+ :realm => :realm,
8
+ :qop => :qop,
9
+ :algorithm => :algorithm,
10
+ :uri => :uri,
11
+ :nonce => :nonce,
12
+ :nc => :nc,
13
+ :cnonce => :cnonce,
14
+ :response => :response,
15
+ }
16
+
17
+ def parse(data, forced_options = {})
18
+ options = HashWithIndifferentAccess.new
19
+ data.to_s.split(/,\s*/).each do |query|
20
+ key, val = query.split('=', 2)
21
+ if valid_key_name = PARAMETER_VALID_KEY_MAPPINGS[key.to_s.intern]
22
+ options[valid_key_name] = val ? val.to_s.delete('"') : nil
23
+ # ActionController::Base.logger.debug("parse: %s => %s" % [valid_key_name,options[valid_key_name]])
24
+ end
25
+ end
26
+ new(options.merge(forced_options))
27
+ end
28
+
29
+ def strength
30
+ 100
31
+ end
32
+ end
33
+
34
+ def set_controller(controller)
35
+ options[:uri] = controller.request.path
36
+ options[:method] = controller.request.method.to_s.upcase
37
+ end
38
+
39
+ def nonce
40
+ value = options[:nonce]
41
+ unless value
42
+ time = Time.now.to_i
43
+ etag = options[:session_key] || random(16)
44
+ pkey = options[:private_key] || random(16)
45
+ value = Base64.encode64("%s:%s:%s" % [time, etag, pkey]).chomp
46
+ end
47
+ return value
48
+ end
49
+
50
+ def algorithm
51
+ options[:algorithm] ||= "MD5"
52
+ end
53
+
54
+ def qop
55
+ options[:qop] ||= "auth"
56
+ end
57
+
58
+ def uri
59
+ options[:uri] ||= '/'
60
+ end
61
+
62
+ def nc
63
+ options[:nc] ||= '00000001'
64
+ end
65
+
66
+ def cnonce
67
+ options[:cnonce] ||= random(32)
68
+ end
69
+
70
+ def method
71
+ options[:method] ||= "GET"
72
+ end
73
+
74
+ def response
75
+ options[:response]
76
+ end
77
+
78
+ def digest_algorithm
79
+ klass_name = algorithm.to_s.upcase
80
+ if klass_name == "SHA1"
81
+ ::Digest::SHA1
82
+ else
83
+ ::Digest::MD5
84
+ end
85
+ end
86
+
87
+ # A1 = unq(username-value) ":" unq(realm-value) ":" passwd
88
+ def a1
89
+ digest_algorithm.hexdigest([user, realm, pass] * ":")
90
+ end
91
+
92
+ # A2 = Method ":" digest-uri-value
93
+ def a2
94
+ # debug_digest("a2", %w( method uri))
95
+ digest_algorithm.hexdigest([method, uri] * ":")
96
+ end
97
+
98
+ #request-digest = <"> < KD ( H(A1), unq(nonce-value)
99
+ # ":" nc-value
100
+ # ":" unq(cnonce-value)
101
+ # ":" unq(qop-value)
102
+ # ":" H(A2)
103
+ # ) <">
104
+ def request_digest
105
+ # debug_digest("request_digest", %w( a1 nonce nc cnonce qop a2 ))
106
+ digest_algorithm.hexdigest([a1, nonce, nc, cnonce, qop, a2] * ":")
107
+ end
108
+
109
+ def response_digest(htdigest)
110
+ # debug_digest("response_digest", %w( htdigest nonce nc cnonce qop a2 ), :htdigest=>htdigest)
111
+ digest_algorithm.hexdigest([htdigest, nonce, nc, cnonce, qop, a2] * ":")
112
+ end
113
+
114
+ def server_header
115
+ %Q|Digest realm="%s", nonce="%s", algorithm=%s, qop=%s| % [realm, nonce, algorithm, qop]
116
+ end
117
+
118
+ def client_header
119
+ %Q|Digest username="%s", realm="%s", qop=%s, algorithm=%s, uri="%s", nonce="%s", nc=%s, cnonce="%s", response="%s"| %
120
+ [user, realm, qop, algorithm, uri, nonce, nc, cnonce, request_digest]
121
+ end
122
+
123
+ ######################################################################
124
+ ### Debug
125
+ def debug_digest(name, element_names, values = {})
126
+ sources = []
127
+ max = element_names.map(&:size).max
128
+ element_names.each do |method|
129
+ value = values[method.intern] || self.send(method)
130
+ logger.debug("[DIGEST] %s#%-*s : %s" % [name, max, method, value])
131
+ sources << value
132
+ end
133
+ digest = digest_algorithm.hexdigest(sources * ':')
134
+ logger.debug("[DIGEST] %s => : %s" % [name, digest])
135
+ return digest
136
+ end
137
+
138
+ ######################################################################
139
+ ### Aliases
140
+ def entry
141
+ [user, realm, a1] * ':'
142
+ end
143
+
144
+ ######################################################################
145
+ ### Authorization
146
+ def authorize_pass(pass)
147
+ response_digest(pass) == response or raise IncorrectPassword
148
+ end
149
+ end
150
+ @@schemas["Digest"] = Digest
151
+ end
152
+ end
@@ -0,0 +1,80 @@
1
+ module Htpasswd
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ class Error < StandardError; end
7
+ class HeaderNotFound < Error; end
8
+ class UnknownSchemeError < Error; end
9
+ class NotAuthorizedError < Error; end
10
+ class ConfigurationError < Error; end
11
+
12
+ class UnknownAccessControl < ConfigurationError; end
13
+ class AuthSchemesNotDefined < ConfigurationError; end
14
+ class IncorrectPassword < NotAuthorizedError; end
15
+ class UnknownUserAccount < NotAuthorizedError; end
16
+
17
+ module ClassMethods
18
+ def htpasswd(options={})
19
+ options = options.dup
20
+ options[:user] ||= options[:username]
21
+ options[:pass] ||= options[:password]
22
+ options[:acl] ||= options[:type]
23
+
24
+ if options[:exclusive]
25
+ write_inheritable_array(:htpasswd_acls, [])
26
+ write_inheritable_hash(:htpasswd_options, {})
27
+ end
28
+
29
+ (htpasswd_options[:schemes] ||= Set.new) << (options[:scheme] || :basic)
30
+
31
+ options[:acl] ||= (options[:class] && :active_record)
32
+ options[:acl] ||= (options[:file] && :htpasswd)
33
+ options[:acl] ||= (options[:user] && options[:pass] && :plain)
34
+
35
+ htpasswd_acls << Acls::Base[options[:acl]].new(options) if options[:acl]
36
+ htpasswd_options.merge!(:realm=>options[:realm]) if options[:realm]
37
+
38
+ skip_before_filter :htpasswd_authorize rescue nil
39
+ delegate :htpasswd_options, :htpasswd_acls, :to=>"self.class" unless instance_methods.include?("htpasswd_options")
40
+ before_filter :htpasswd_authorize
41
+ end
42
+
43
+ def htdigest(options={})
44
+ options = options.dup
45
+ if options[:user] && options[:pass] && (options[:acl] || options[:type]) != :crypted && options[:class].blank?
46
+ options[:pass] = Auths::Digest.new(options).a1
47
+ end
48
+ options[:scheme] = :digest
49
+ options[:acl] ||= (options[:class] && :active_record)
50
+ options[:acl] ||= (options[:file] ? :htdigest : :digest)
51
+
52
+ htpasswd(options)
53
+ end
54
+
55
+ def htpasswd_options
56
+ read_inheritable_attribute(:htpasswd_options) or write_inheritable_hash(:htpasswd_options, {})
57
+ end
58
+
59
+ def htpasswd_acls
60
+ read_inheritable_attribute(:htpasswd_acls) or write_inheritable_array(:htpasswd_acls, [])
61
+ end
62
+ end
63
+
64
+ protected
65
+ def htpasswd_authorize
66
+ logger.debug "Htpasswd is enabled with %s" % htpasswd_options.inspect
67
+ username = Auths.scheme(self).authorize(htpasswd_acls)
68
+ logger.debug "Htpasswd authorize user '%s'" % username
69
+ @htpasswd_authorized_username = username
70
+ return true
71
+ rescue Htpasswd::Error => error
72
+ logger.debug "Htpasswd error(%s): %s" % [error.class, error.message]
73
+ strongest_auth = htpasswd_options[:schemes].map{|scheme| Auths[scheme]}.sort.last or raise AuthSchemesNotDefined
74
+ response.headers['WWW-Authenticate'] = strongest_auth.new(htpasswd_options).server_header
75
+ logger.debug "Htpasswd sending authenticate header: '%s'"% response.headers['WWW-Authenticate']
76
+
77
+ render :nothing => true, :status => 401
78
+ return false
79
+ end
80
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :htpasswd do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,38 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class DigestTestCase < Test::Unit::TestCase
4
+ def test_calculate_request_digest
5
+ options = {
6
+ :username => "maiha",
7
+ :password => "berryz",
8
+ :realm => "Authorization",
9
+ :nonce => "MTE0ODYxODUxNDozNGJhZDVhMzQ2YjBiM2QzOmQwMTVhYjdlNGJkZjkzMjk=",
10
+ :uri => "/test",
11
+ :algorithm => "MD5",
12
+ :cnonce => "cnonce",
13
+ :qop => "auth",
14
+ :nc => "00000001",
15
+ }
16
+ expected = "4d7849b4faab53fd3eff731c24cc24d1"
17
+ digest = Htpasswd::Authorization::Digest.new(options)
18
+ assert_equal expected, digest.request_digest
19
+ end
20
+
21
+ def test_calculate_response_digest
22
+ options = {
23
+ :username => "maiha",
24
+ :password => "berryz",
25
+ :realm => "Authorization",
26
+ :nonce => "MTE0ODYxODUxNDozNGJhZDVhMzQ2YjBiM2QzOmQwMTVhYjdlNGJkZjkzMjk=",
27
+ :uri => "/test",
28
+ :algorithm => "MD5",
29
+ :cnonce => "cnonce",
30
+ :qop => "auth",
31
+ :nc => "00000001",
32
+ }
33
+ expected = "4d7849b4faab53fd3eff731c24cc24d1"
34
+ htdigest = "812b1d067e9ce1e44f09215339e3cd69"
35
+ digest = Htpasswd::Authorization::Digest.new(options)
36
+ assert_equal expected, digest.response_digest(htdigest)
37
+ end
38
+ end
@@ -0,0 +1,2 @@
1
+ maiha:Authorization:812b1d067e9ce1e44f09215339e3cd69
2
+ saki:Authorization:6dbfe8844106740d4b5d23a1553b3d90
@@ -0,0 +1,3 @@
1
+ airi:Authorization:553ff400cba5d59d47bb2601cbc2c09e
2
+
3
+ maimi:Authorization:15357bf5d26f0c6046729d8d03e0b320
@@ -0,0 +1,2 @@
1
+ maiha:7Et1Y7tCawx32
2
+ saki:pqebMmYD7swcM
@@ -0,0 +1,2 @@
1
+ airi:FKy4zBgGsUMpc
2
+ maimi:3A.pVZRrenahQ
@@ -0,0 +1,211 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class TestController < ActionController::Base
4
+ def self.external_password_path(file)
5
+ File.dirname(__FILE__) + '/fixtures/' + file
6
+ end
7
+
8
+ def index
9
+ render :nothing=>true
10
+ end
11
+ end
12
+
13
+ class HtdigestTestCase < Test::Unit::TestCase
14
+ class Controller < TestController; end
15
+
16
+ def setup
17
+ @request = ActionController::TestRequest.new
18
+ @response = ActionController::TestResponse.new
19
+ @controller = self.class::Controller.new
20
+ end
21
+
22
+ def digest_authorization(username, password, method = :get)
23
+ send(method, :index)
24
+ digest = Htpasswd::Auths::Digest.parse(@response.headers['WWW-Authenticate'])
25
+ digest.set_controller(@controller)
26
+ digest.options[:user] = username
27
+ digest.options[:pass] = password
28
+
29
+ setup
30
+ @request.env["HTTP_AUTHORIZATION"] = digest.client_header
31
+ end
32
+
33
+ def test_dummy
34
+ end
35
+ end
36
+
37
+
38
+ class InlineEntryTest < HtdigestTestCase
39
+ class Controller < TestController
40
+ htdigest :user=>"maiha", :pass=>"berryz", :realm=>"Authorization"
41
+ end
42
+
43
+ def test_content_should_be_authorized
44
+ get :index
45
+ assert_response 401
46
+ assert /\ADigest / === @response.headers["WWW-Authenticate"].to_s
47
+ end
48
+
49
+ def test_authorize_valid_request
50
+ digest_authorization('maiha', 'berryz')
51
+ get :index
52
+ assert_response :success
53
+ end
54
+
55
+ def test_authorize_valid_request
56
+ digest_authorization('maiha', 'berryz')
57
+ get :index
58
+ assert_response :success
59
+ end
60
+ end
61
+
62
+
63
+ class InlineMultiEntriesTest < HtdigestTestCase
64
+ class Controller < TestController
65
+ htdigest :user=>"maiha", :pass=>"berryz"
66
+ htdigest :user=>"airi", :pass=>"cute"
67
+ end
68
+
69
+ def test_content_should_be_authorized
70
+ get :index
71
+ assert_response 401
72
+ end
73
+
74
+ def test_authorize_valid_request_written_in_first_entry
75
+ digest_authorization('maiha', 'berryz')
76
+ get :index
77
+ assert_response :success
78
+ end
79
+
80
+ def test_authorize_valid_request_written_in_second_entry
81
+ digest_authorization('airi', 'cute')
82
+ get :index
83
+ assert_response :success
84
+ end
85
+
86
+ def test_authorize_invalid_request
87
+ digest_authorization('maiha', 'xxx')
88
+ get :index
89
+ assert_response 401
90
+ end
91
+ end
92
+
93
+
94
+ class InlineCryptedEntryTest < HtdigestTestCase
95
+ class Controller < TestController
96
+ htdigest :user=>"maiha", :pass=>"812b1d067e9ce1e44f09215339e3cd69", :realm=>"Authorization", :type=>:crypted
97
+ end
98
+
99
+ def test_authorize_valid_request
100
+ digest_authorization('maiha', 'berryz')
101
+ get :index
102
+ assert_response :success
103
+ end
104
+
105
+ def test_authorize_invalid_request
106
+ digest_authorization('maiha', 'xxx')
107
+ get :index
108
+ assert_response 401
109
+ end
110
+ end
111
+
112
+
113
+ class ExternalFileTest < HtdigestTestCase
114
+ class Controller < TestController
115
+ htdigest :file=>external_password_path('htdigest.berryz')
116
+ end
117
+
118
+ def test_content_should_be_authorized
119
+ get :index
120
+ assert_response 401
121
+ end
122
+
123
+ def test_authorize_valid_request_written_in_first_entry
124
+ digest_authorization('maiha', 'berryz')
125
+ get :index
126
+ assert_response :success
127
+ end
128
+
129
+ def test_authorize_valid_request_written_in_second_entry
130
+ digest_authorization('saki', 'berryz')
131
+ get :index
132
+ assert_response :success
133
+ end
134
+
135
+ def test_authorize_invalid_request
136
+ digest_authorization('airi', 'cute')
137
+ get :index
138
+ assert_response 401
139
+ end
140
+ end
141
+
142
+
143
+ class CompositeTest < HtdigestTestCase
144
+ class Controller < TestController
145
+ htdigest :user=>"risako" , :pass=>"berryz"
146
+ htdigest :user=>"yurina" , :pass=>"f278b3900164e31fcf005a79a8b2da2e", :type=>:crypted
147
+ htdigest :file=>external_password_path('htdigest.berryz')
148
+ htdigest :file=>external_password_path('htdigest.cute')
149
+ end
150
+
151
+ def test_content_should_be_authorized
152
+ get :index
153
+ assert_response 401
154
+ end
155
+
156
+ def test_authorize_inline_entry
157
+ digest_authorization('risako', 'berryz')
158
+ get :index
159
+ assert_response :success
160
+ end
161
+
162
+ def test_authorize_inline_crypted_entry
163
+ digest_authorization('yurina', 'berryz')
164
+ get :index
165
+ assert_response :success
166
+ end
167
+
168
+ def test_authorize_external_first_file
169
+ # first entry
170
+ digest_authorization('maiha', 'berryz')
171
+ get :index
172
+ assert_response :success
173
+
174
+ # second entry
175
+ setup
176
+ digest_authorization('saki', 'berryz')
177
+ get :index
178
+ assert_response :success
179
+ end
180
+
181
+ def test_authorize_external_second_file
182
+ # first entry
183
+ digest_authorization('airi', 'cute')
184
+ get :index
185
+ assert_response :success
186
+
187
+ # second entry
188
+ setup
189
+ digest_authorization('maimi', 'cute')
190
+ get :index
191
+ assert_response :success
192
+ end
193
+
194
+ def test_authorize_invalid_request
195
+ digest_authorization('maiha', 'cute')
196
+ get :index
197
+ assert_response 401
198
+ end
199
+ end
200
+
201
+ class InlineEntryTest < HtdigestTestCase
202
+ class Controller < TestController
203
+ htdigest :user=>"maiha" , :pass=>"berryz"
204
+ end
205
+
206
+ def test_authorize_with_post
207
+ digest_authorization('maiha', 'berryz', :post)
208
+ post :index
209
+ assert_response :success
210
+ end
211
+ end
@@ -0,0 +1,210 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class TestController < ActionController::Base
4
+ def self.external_password_path(file)
5
+ File.dirname(__FILE__) + '/fixtures/' + file
6
+ end
7
+
8
+ def index
9
+ render :nothing=>true
10
+ end
11
+ end
12
+
13
+ class HtpasswdTestCase < Test::Unit::TestCase
14
+ class Controller < TestController; end
15
+
16
+ def setup
17
+ @request = ActionController::TestRequest.new
18
+ @response = ActionController::TestResponse.new
19
+ @controller = self.class::Controller.new
20
+ end
21
+
22
+ def basic_authorization(user, pass)
23
+ @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
24
+ end
25
+
26
+ def test_dummy
27
+ end
28
+ end
29
+
30
+
31
+ class InlineEntryTest < HtpasswdTestCase
32
+ class Controller < TestController
33
+ htpasswd :user=>"maiha", :pass=>"berryz"
34
+ end
35
+
36
+ def test_content_should_be_authorized
37
+ get :index
38
+ assert_response 401
39
+ assert_equal 'Basic realm="Authorization"', @response.headers["WWW-Authenticate"].to_s
40
+ end
41
+
42
+ def test_authorize_valid_request
43
+ basic_authorization('maiha', 'berryz')
44
+ get :index
45
+ assert_response :success
46
+ end
47
+
48
+ def test_authorize_invalid_request
49
+ basic_authorization('maiha', 'xxx')
50
+ get :index
51
+ assert_response 401
52
+ end
53
+ end
54
+
55
+
56
+ class InlineMultiEntriesTest < HtpasswdTestCase
57
+ class Controller < TestController
58
+ htpasswd :user=>"maiha", :pass=>"berryz"
59
+ htpasswd :user=>"airi", :pass=>"cute"
60
+ end
61
+
62
+ def test_content_should_be_authorized
63
+ get :index
64
+ assert_response 401
65
+ end
66
+
67
+ def test_authorize_valid_request_written_in_first_entry
68
+ basic_authorization('maiha', 'berryz')
69
+ get :index
70
+ assert_response :success
71
+ end
72
+
73
+ def test_authorize_valid_request_written_in_second_entry
74
+ basic_authorization('airi', 'cute')
75
+ get :index
76
+ assert_response :success
77
+ end
78
+
79
+ def test_authorize_invalid_request
80
+ basic_authorization('maiha', 'xxx')
81
+ get :index
82
+ assert_response 401
83
+ end
84
+ end
85
+
86
+
87
+ class InlineCryptedEntryTest < HtpasswdTestCase
88
+ class Controller < TestController
89
+ htpasswd :user=>"maiha", :pass=>"7Et1Y7tCawx32", :type=>:crypted
90
+ end
91
+
92
+ def test_content_should_be_authorized
93
+ get :index
94
+ assert_response 401
95
+ end
96
+
97
+ def test_authorize_valid_request
98
+ basic_authorization('maiha', 'berryz')
99
+ get :index
100
+ assert_response :success
101
+ end
102
+
103
+ def test_authorize_invalid_request
104
+ basic_authorization('maiha', 'xxx')
105
+ get :index
106
+ assert_response 401
107
+ end
108
+ end
109
+
110
+
111
+ class ExternalFileTest < HtpasswdTestCase
112
+ class Controller < TestController
113
+ htpasswd :file=>external_password_path('htpasswd.berryz')
114
+ end
115
+
116
+ def test_content_should_be_authorized
117
+ get :index
118
+ assert_response 401
119
+ end
120
+
121
+ def test_authorize_valid_request_written_in_first_entry
122
+ basic_authorization('maiha', 'berryz')
123
+ get :index
124
+ assert_response :success
125
+ end
126
+
127
+ def test_authorize_valid_request_written_in_second_entry
128
+ basic_authorization('saki', 'berryz')
129
+ get :index
130
+ assert_response :success
131
+ end
132
+
133
+ def test_authorize_invalid_request
134
+ basic_authorization('airi', 'cute')
135
+ get :index
136
+ assert_response 401
137
+ end
138
+ end
139
+
140
+
141
+ class CompositeTest < HtpasswdTestCase
142
+ class Controller < TestController
143
+ htpasswd :user=>"risako" , :pass=>"berryz"
144
+ htpasswd :user=>"yurina" , :pass=>"7Et1Y7tCawx32", :type=>:crypted
145
+ htpasswd :file=>external_password_path('htpasswd.berryz')
146
+ htpasswd :file=>external_password_path('htpasswd.cute')
147
+ end
148
+
149
+ def test_content_should_be_authorized
150
+ get :index
151
+ assert_response 401
152
+ end
153
+
154
+ def test_authorize_inline_entry
155
+ basic_authorization('risako', 'berryz')
156
+ get :index
157
+ assert_response :success
158
+ end
159
+
160
+ def test_authorize_inline_crypted_entry
161
+ basic_authorization('yurina', 'berryz')
162
+ get :index
163
+ assert_response :success
164
+ end
165
+
166
+ def test_authorize_external_first_file
167
+ # first entry
168
+ basic_authorization('maiha', 'berryz')
169
+ get :index
170
+ assert_response :success
171
+
172
+ # second entry
173
+ setup
174
+ basic_authorization('saki', 'berryz')
175
+ get :index
176
+ assert_response :success
177
+ end
178
+
179
+ def test_authorize_external_second_file
180
+ # first entry
181
+ basic_authorization('airi', 'cute')
182
+ get :index
183
+ assert_response :success
184
+
185
+ # second entry
186
+ setup
187
+ basic_authorization('maimi', 'cute')
188
+ get :index
189
+ assert_response :success
190
+ end
191
+
192
+ def test_authorize_invalid_request
193
+ basic_authorization('maiha', 'cute')
194
+ get :index
195
+ assert_response 401
196
+ end
197
+ end
198
+
199
+
200
+ class RealmTest < HtpasswdTestCase
201
+ class Controller < TestController
202
+ htpasswd :user=>"maiha", :pass=>"berryz", :realm=>"Member Only"
203
+ end
204
+
205
+ def test_set_realm
206
+ get :index
207
+ assert_equal 'Basic realm="Member Only"', @response.headers["WWW-Authenticate"].to_s
208
+ end
209
+ end
210
+
@@ -0,0 +1,25 @@
1
+ def __DIR__; File.dirname(__FILE__); end
2
+
3
+ require 'test/unit'
4
+ $:.unshift(__DIR__ + '/../lib')
5
+ begin
6
+ require 'rubygems'
7
+ rescue LoadError
8
+ $:.unshift(__DIR__ + '/../../../rails/activesupport/lib')
9
+ $:.unshift(__DIR__ + '/../../../rails/actionpack/lib')
10
+ end
11
+
12
+ require 'active_support'
13
+ require 'action_controller'
14
+ require 'action_controller/test_process'
15
+
16
+ ActionController::Routing::Routes.reload rescue nil
17
+ class ActionController::Base; def rescue_action(e) raise e end; end
18
+
19
+ logfile = __DIR__ + '/debug.log'
20
+ File.unlink(logfile) if File.exists?(logfile)
21
+ logger = Logger.new(logfile)
22
+ logger.level = Logger::DEBUG
23
+ ActionController::Base.logger = logger
24
+
25
+ require __DIR__ + '/../init'
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: maiha-htpasswd
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - maiha
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-08 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: ""
17
+ email: maiha@wota.jp
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - README
26
+ - Rakefile
27
+ - init.rb
28
+ - install.rb
29
+ - lib/htpasswd/acls/active_record.rb
30
+ - lib/htpasswd/acls/base.rb
31
+ - lib/htpasswd/acls/crypted.rb
32
+ - lib/htpasswd/acls/digest.rb
33
+ - lib/htpasswd/acls/htdigest.rb
34
+ - lib/htpasswd/acls/htpasswd.rb
35
+ - lib/htpasswd/acls/plain.rb
36
+ - lib/htpasswd/auths/base.rb
37
+ - lib/htpasswd/auths/basic.rb
38
+ - lib/htpasswd/auths/digest.rb
39
+ - lib/htpasswd/class_methods.rb
40
+ - tasks/htpasswd_tasks.rake
41
+ - test/digest_test.rb
42
+ - test/fixtures/htdigest.berryz
43
+ - test/fixtures/htdigest.cute
44
+ - test/fixtures/htpasswd.berryz
45
+ - test/fixtures/htpasswd.cute
46
+ - test/htdigest_test.rb
47
+ - test/htpasswd_test.rb
48
+ - test/test_helper.rb
49
+ has_rdoc: true
50
+ homepage: http://github.com/maiha/htpasswd
51
+ post_install_message:
52
+ rdoc_options: []
53
+
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ requirements: []
69
+
70
+ rubyforge_project:
71
+ rubygems_version: 1.2.0
72
+ signing_key:
73
+ specification_version: 2
74
+ summary: ""
75
+ test_files: []
76
+