kastner-rack 0.3.171

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. data/AUTHORS +8 -0
  2. data/COPYING +18 -0
  3. data/KNOWN-ISSUES +18 -0
  4. data/README +273 -0
  5. data/Rakefile +185 -0
  6. data/bin/rackup +172 -0
  7. data/contrib/rack_logo.svg +111 -0
  8. data/example/lobster.ru +4 -0
  9. data/example/protectedlobster.rb +14 -0
  10. data/example/protectedlobster.ru +8 -0
  11. data/lib/rack.rb +85 -0
  12. data/lib/rack/adapter/camping.rb +22 -0
  13. data/lib/rack/auth/abstract/handler.rb +28 -0
  14. data/lib/rack/auth/abstract/request.rb +37 -0
  15. data/lib/rack/auth/basic.rb +58 -0
  16. data/lib/rack/auth/digest/md5.rb +124 -0
  17. data/lib/rack/auth/digest/nonce.rb +51 -0
  18. data/lib/rack/auth/digest/params.rb +55 -0
  19. data/lib/rack/auth/digest/request.rb +40 -0
  20. data/lib/rack/auth/openid.rb +437 -0
  21. data/lib/rack/builder.rb +67 -0
  22. data/lib/rack/cascade.rb +36 -0
  23. data/lib/rack/commonlogger.rb +61 -0
  24. data/lib/rack/conditionalget.rb +42 -0
  25. data/lib/rack/deflater.rb +63 -0
  26. data/lib/rack/directory.rb +149 -0
  27. data/lib/rack/file.rb +84 -0
  28. data/lib/rack/handler.rb +46 -0
  29. data/lib/rack/handler/cgi.rb +57 -0
  30. data/lib/rack/handler/evented_mongrel.rb +8 -0
  31. data/lib/rack/handler/fastcgi.rb +86 -0
  32. data/lib/rack/handler/lsws.rb +52 -0
  33. data/lib/rack/handler/mongrel.rb +78 -0
  34. data/lib/rack/handler/scgi.rb +57 -0
  35. data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
  36. data/lib/rack/handler/webrick.rb +61 -0
  37. data/lib/rack/head.rb +19 -0
  38. data/lib/rack/lint.rb +463 -0
  39. data/lib/rack/lobster.rb +65 -0
  40. data/lib/rack/methodoverride.rb +21 -0
  41. data/lib/rack/mime.rb +204 -0
  42. data/lib/rack/mock.rb +160 -0
  43. data/lib/rack/recursive.rb +57 -0
  44. data/lib/rack/reloader.rb +64 -0
  45. data/lib/rack/request.rb +217 -0
  46. data/lib/rack/response.rb +171 -0
  47. data/lib/rack/session/abstract/id.rb +140 -0
  48. data/lib/rack/session/cookie.rb +89 -0
  49. data/lib/rack/session/memcache.rb +97 -0
  50. data/lib/rack/session/pool.rb +73 -0
  51. data/lib/rack/showexceptions.rb +348 -0
  52. data/lib/rack/showstatus.rb +105 -0
  53. data/lib/rack/static.rb +38 -0
  54. data/lib/rack/urlmap.rb +48 -0
  55. data/lib/rack/utils.rb +318 -0
  56. data/rack.gemspec +31 -0
  57. data/test/cgi/lighttpd.conf +20 -0
  58. data/test/cgi/test +9 -0
  59. data/test/cgi/test.fcgi +8 -0
  60. data/test/cgi/test.ru +7 -0
  61. data/test/spec_rack_auth_basic.rb +69 -0
  62. data/test/spec_rack_auth_digest.rb +169 -0
  63. data/test/spec_rack_auth_openid.rb +137 -0
  64. data/test/spec_rack_builder.rb +84 -0
  65. data/test/spec_rack_camping.rb +51 -0
  66. data/test/spec_rack_cascade.rb +50 -0
  67. data/test/spec_rack_cgi.rb +89 -0
  68. data/test/spec_rack_commonlogger.rb +32 -0
  69. data/test/spec_rack_conditionalget.rb +41 -0
  70. data/test/spec_rack_deflater.rb +70 -0
  71. data/test/spec_rack_directory.rb +56 -0
  72. data/test/spec_rack_fastcgi.rb +89 -0
  73. data/test/spec_rack_file.rb +57 -0
  74. data/test/spec_rack_handler.rb +24 -0
  75. data/test/spec_rack_head.rb +30 -0
  76. data/test/spec_rack_lint.rb +371 -0
  77. data/test/spec_rack_lobster.rb +45 -0
  78. data/test/spec_rack_methodoverride.rb +31 -0
  79. data/test/spec_rack_mock.rb +152 -0
  80. data/test/spec_rack_mongrel.rb +170 -0
  81. data/test/spec_rack_recursive.rb +77 -0
  82. data/test/spec_rack_request.rb +426 -0
  83. data/test/spec_rack_response.rb +173 -0
  84. data/test/spec_rack_session_cookie.rb +78 -0
  85. data/test/spec_rack_session_memcache.rb +132 -0
  86. data/test/spec_rack_session_pool.rb +84 -0
  87. data/test/spec_rack_showexceptions.rb +21 -0
  88. data/test/spec_rack_showstatus.rb +72 -0
  89. data/test/spec_rack_static.rb +37 -0
  90. data/test/spec_rack_urlmap.rb +175 -0
  91. data/test/spec_rack_utils.rb +174 -0
  92. data/test/spec_rack_webrick.rb +123 -0
  93. data/test/testrequest.rb +45 -0
  94. metadata +177 -0
@@ -0,0 +1,37 @@
1
+ module Rack
2
+ module Auth
3
+ class AbstractRequest
4
+
5
+ def initialize(env)
6
+ @env = env
7
+ end
8
+
9
+ def provided?
10
+ !authorization_key.nil?
11
+ end
12
+
13
+ def parts
14
+ @parts ||= @env[authorization_key].split(' ', 2)
15
+ end
16
+
17
+ def scheme
18
+ @scheme ||= parts.first.downcase.to_sym
19
+ end
20
+
21
+ def params
22
+ @params ||= parts.last
23
+ end
24
+
25
+
26
+ private
27
+
28
+ AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION']
29
+
30
+ def authorization_key
31
+ @authorization_key ||= AUTHORIZATION_KEYS.detect { |key| @env.has_key?(key) }
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,58 @@
1
+ require 'rack/auth/abstract/handler'
2
+ require 'rack/auth/abstract/request'
3
+
4
+ module Rack
5
+ module Auth
6
+ # Rack::Auth::Basic implements HTTP Basic Authentication, as per RFC 2617.
7
+ #
8
+ # Initialize with the Rack application that you want protecting,
9
+ # and a block that checks if a username and password pair are valid.
10
+ #
11
+ # See also: <tt>example/protectedlobster.rb</tt>
12
+
13
+ class Basic < AbstractHandler
14
+
15
+ def call(env)
16
+ auth = Basic::Request.new(env)
17
+
18
+ return unauthorized unless auth.provided?
19
+
20
+ return bad_request unless auth.basic?
21
+
22
+ if valid?(auth)
23
+ env['REMOTE_USER'] = auth.username
24
+
25
+ return @app.call(env)
26
+ end
27
+
28
+ unauthorized
29
+ end
30
+
31
+
32
+ private
33
+
34
+ def challenge
35
+ 'Basic realm="%s"' % realm
36
+ end
37
+
38
+ def valid?(auth)
39
+ @authenticator.call(*auth.credentials)
40
+ end
41
+
42
+ class Request < Auth::AbstractRequest
43
+ def basic?
44
+ :basic == scheme
45
+ end
46
+
47
+ def credentials
48
+ @credentials ||= params.unpack("m*").first.split(/:/, 2)
49
+ end
50
+
51
+ def username
52
+ credentials.first
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,124 @@
1
+ require 'rack/auth/abstract/handler'
2
+ require 'rack/auth/digest/request'
3
+ require 'rack/auth/digest/params'
4
+ require 'rack/auth/digest/nonce'
5
+ require 'digest/md5'
6
+
7
+ module Rack
8
+ module Auth
9
+ module Digest
10
+ # Rack::Auth::Digest::MD5 implements the MD5 algorithm version of
11
+ # HTTP Digest Authentication, as per RFC 2617.
12
+ #
13
+ # Initialize with the [Rack] application that you want protecting,
14
+ # and a block that looks up a plaintext password for a given username.
15
+ #
16
+ # +opaque+ needs to be set to a constant base64/hexadecimal string.
17
+ #
18
+ class MD5 < AbstractHandler
19
+
20
+ attr_accessor :opaque
21
+
22
+ attr_writer :passwords_hashed
23
+
24
+ def initialize(app)
25
+ super
26
+ @passwords_hashed = nil
27
+ end
28
+
29
+ def passwords_hashed?
30
+ !!@passwords_hashed
31
+ end
32
+
33
+ def call(env)
34
+ auth = Request.new(env)
35
+
36
+ unless auth.provided?
37
+ return unauthorized
38
+ end
39
+
40
+ if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth)
41
+ return bad_request
42
+ end
43
+
44
+ if valid?(auth)
45
+ if auth.nonce.stale?
46
+ return unauthorized(challenge(:stale => true))
47
+ else
48
+ env['REMOTE_USER'] = auth.username
49
+
50
+ return @app.call(env)
51
+ end
52
+ end
53
+
54
+ unauthorized
55
+ end
56
+
57
+
58
+ private
59
+
60
+ QOP = 'auth'.freeze
61
+
62
+ def params(hash = {})
63
+ Params.new do |params|
64
+ params['realm'] = realm
65
+ params['nonce'] = Nonce.new.to_s
66
+ params['opaque'] = H(opaque)
67
+ params['qop'] = QOP
68
+
69
+ hash.each { |k, v| params[k] = v }
70
+ end
71
+ end
72
+
73
+ def challenge(hash = {})
74
+ "Digest #{params(hash)}"
75
+ end
76
+
77
+ def valid?(auth)
78
+ valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth)
79
+ end
80
+
81
+ def valid_qop?(auth)
82
+ QOP == auth.qop
83
+ end
84
+
85
+ def valid_opaque?(auth)
86
+ H(opaque) == auth.opaque
87
+ end
88
+
89
+ def valid_nonce?(auth)
90
+ auth.nonce.valid?
91
+ end
92
+
93
+ def valid_digest?(auth)
94
+ digest(auth, @authenticator.call(auth.username)) == auth.response
95
+ end
96
+
97
+ def md5(data)
98
+ ::Digest::MD5.hexdigest(data)
99
+ end
100
+
101
+ alias :H :md5
102
+
103
+ def KD(secret, data)
104
+ H([secret, data] * ':')
105
+ end
106
+
107
+ def A1(auth, password)
108
+ [ auth.username, auth.realm, password ] * ':'
109
+ end
110
+
111
+ def A2(auth)
112
+ [ auth.method, auth.uri ] * ':'
113
+ end
114
+
115
+ def digest(auth, password)
116
+ password_hash = passwords_hashed? ? password : H(A1(auth, password))
117
+
118
+ KD(password_hash, [ auth.nonce, auth.nc, auth.cnonce, QOP, H(A2(auth)) ] * ':')
119
+ end
120
+
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,51 @@
1
+ require 'digest/md5'
2
+
3
+ module Rack
4
+ module Auth
5
+ module Digest
6
+ # Rack::Auth::Digest::Nonce is the default nonce generator for the
7
+ # Rack::Auth::Digest::MD5 authentication handler.
8
+ #
9
+ # +private_key+ needs to set to a constant string.
10
+ #
11
+ # +time_limit+ can be optionally set to an integer (number of seconds),
12
+ # to limit the validity of the generated nonces.
13
+
14
+ class Nonce
15
+
16
+ class << self
17
+ attr_accessor :private_key, :time_limit
18
+ end
19
+
20
+ def self.parse(string)
21
+ new(*string.unpack("m*").first.split(' ', 2))
22
+ end
23
+
24
+ def initialize(timestamp = Time.now, given_digest = nil)
25
+ @timestamp, @given_digest = timestamp.to_i, given_digest
26
+ end
27
+
28
+ def to_s
29
+ [([ @timestamp, digest ] * ' ')].pack("m*").strip
30
+ end
31
+
32
+ def digest
33
+ ::Digest::MD5.hexdigest([ @timestamp, self.class.private_key ] * ':')
34
+ end
35
+
36
+ def valid?
37
+ digest == @given_digest
38
+ end
39
+
40
+ def stale?
41
+ !self.class.time_limit.nil? && (@timestamp - Time.now.to_i) < self.class.time_limit
42
+ end
43
+
44
+ def fresh?
45
+ !stale?
46
+ end
47
+
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ module Rack
2
+ module Auth
3
+ module Digest
4
+ class Params < Hash
5
+
6
+ def self.parse(str)
7
+ split_header_value(str).inject(new) do |header, param|
8
+ k, v = param.split('=', 2)
9
+ header[k] = dequote(v)
10
+ header
11
+ end
12
+ end
13
+
14
+ def self.dequote(str) # From WEBrick::HTTPUtils
15
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
16
+ ret.gsub!(/\\(.)/, "\\1")
17
+ ret
18
+ end
19
+
20
+ def self.split_header_value(str)
21
+ str.scan( /(\w+\=(?:"[^\"]+"|[^,]+))/n ).collect{ |v| v[0] }
22
+ end
23
+
24
+ def initialize
25
+ super
26
+
27
+ yield self if block_given?
28
+ end
29
+
30
+ def [](k)
31
+ super k.to_s
32
+ end
33
+
34
+ def []=(k, v)
35
+ super k.to_s, v.to_s
36
+ end
37
+
38
+ UNQUOTED = ['qop', 'nc', 'stale']
39
+
40
+ def to_s
41
+ inject([]) do |parts, (k, v)|
42
+ parts << "#{k}=" + (UNQUOTED.include?(k) ? v.to_s : quote(v))
43
+ parts
44
+ end.join(', ')
45
+ end
46
+
47
+ def quote(str) # From WEBrick::HTTPUtils
48
+ '"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,40 @@
1
+ require 'rack/auth/abstract/request'
2
+ require 'rack/auth/digest/params'
3
+ require 'rack/auth/digest/nonce'
4
+
5
+ module Rack
6
+ module Auth
7
+ module Digest
8
+ class Request < Auth::AbstractRequest
9
+
10
+ def method
11
+ @env['REQUEST_METHOD']
12
+ end
13
+
14
+ def digest?
15
+ :digest == scheme
16
+ end
17
+
18
+ def correct_uri?
19
+ @env['PATH_INFO'] == uri
20
+ end
21
+
22
+ def nonce
23
+ @nonce ||= Nonce.parse(params['nonce'])
24
+ end
25
+
26
+ def params
27
+ @params ||= Params.parse(parts.last)
28
+ end
29
+
30
+ def method_missing(sym)
31
+ if params.has_key? key = sym.to_s
32
+ return params[key]
33
+ end
34
+ super
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,437 @@
1
+ # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
2
+
3
+ gem 'ruby-openid', '~> 2' if defined? Gem
4
+ require 'rack/auth/abstract/handler' #rack
5
+ require 'uri' #std
6
+ require 'pp' #std
7
+ require 'openid' #gem
8
+ require 'openid/extension' #gem
9
+ require 'openid/store/memory' #gem
10
+
11
+ module Rack
12
+ module Auth
13
+ # Rack::Auth::OpenID provides a simple method for permitting
14
+ # openid based logins. It requires the ruby-openid library from
15
+ # janrain to operate, as well as a rack method of session management.
16
+ #
17
+ # The ruby-openid home page is at http://openidenabled.com/ruby-openid/.
18
+ #
19
+ # The OpenID specifications can be found at
20
+ # http://openid.net/specs/openid-authentication-1_1.html
21
+ # and
22
+ # http://openid.net/specs/openid-authentication-2_0.html. Documentation
23
+ # for published OpenID extensions and related topics can be found at
24
+ # http://openid.net/developers/specs/.
25
+ #
26
+ # It is recommended to read through the OpenID spec, as well as
27
+ # ruby-openid's documentation, to understand what exactly goes on. However
28
+ # a setup as simple as the presented examples is enough to provide
29
+ # functionality.
30
+ #
31
+ # This library strongly intends to utilize the OpenID 2.0 features of the
32
+ # ruby-openid library, while maintaining OpenID 1.0 compatiblity.
33
+ #
34
+ # All responses from this rack application will be 303 redirects unless an
35
+ # error occurs, with the exception of an authentication request requiring
36
+ # an HTML form submission.
37
+ #
38
+ # NOTE: Extensions are not currently supported by this implimentation of
39
+ # the OpenID rack application due to the complexity of the current
40
+ # ruby-openid extension handling.
41
+ #
42
+ # NOTE: Due to the amount of data that this library stores in the
43
+ # session, Rack::Session::Cookie may fault.
44
+ class OpenID < AbstractHandler
45
+ class NoSession < RuntimeError; end
46
+ # Required for ruby-openid
47
+ OIDStore = ::OpenID::Store::Memory.new
48
+ HTML = '<html><head><title>%s</title></head><body>%s</body></html>'
49
+
50
+ # A Hash of options is taken as it's single initializing
51
+ # argument. For example:
52
+ #
53
+ # simple_oid = OpenID.new('http://mysite.com/')
54
+ #
55
+ # return_oid = OpenID.new('http://mysite.com/', {
56
+ # :return_to => 'http://mysite.com/openid'
57
+ # })
58
+ #
59
+ # page_oid = OpenID.new('http://mysite.com/',
60
+ # :login_good => 'http://mysite.com/auth_good'
61
+ # )
62
+ #
63
+ # complex_oid = OpenID.new('http://mysite.com/',
64
+ # :return_to => 'http://mysite.com/openid',
65
+ # :login_good => 'http://mysite.com/user/preferences',
66
+ # :auth_fail => [500, {'Content-Type'=>'text/plain'},
67
+ # 'Unable to negotiate with foreign server.'],
68
+ # :immediate => true,
69
+ # :extensions => {
70
+ # ::OpenID::SReg => [['email'],['nickname']]
71
+ # }
72
+ # )
73
+ #
74
+ # = Arguments
75
+ #
76
+ # The first argument is the realm, identifying the site they are trusting
77
+ # with their identity. This is required.
78
+ #
79
+ # NOTE: In OpenID 1.x, the realm or trust_root is optional and the
80
+ # return_to url is required. As this library strives tward ruby-openid
81
+ # 2.0, and OpenID 2.0 compatibiliy, the realm is required and return_to
82
+ # is optional. However, this implimentation is still backwards compatible
83
+ # with OpenID 1.0 servers.
84
+ #
85
+ # The optional second argument is a hash of options.
86
+ #
87
+ # == Options
88
+ #
89
+ # <tt>:return_to</tt> defines the url to return to after the client
90
+ # authenticates with the openid service provider. This url should point
91
+ # to where Rack::Auth::OpenID is mounted. If <tt>:return_to</tt> is not
92
+ # provided, :return_to will be the current url including all query
93
+ # parameters.
94
+ #
95
+ # <tt>:session_key</tt> defines the key to the session hash in the env.
96
+ # It defaults to 'rack.session'.
97
+ #
98
+ # <tt>:openid_param</tt> defines at what key in the request parameters to
99
+ # find the identifier to resolve. As per the 2.0 spec, the default is
100
+ # 'openid_identifier'.
101
+ #
102
+ # <tt>:immediate</tt> as true will make immediate type of requests the
103
+ # default. See OpenID specification documentation.
104
+ #
105
+ # === URL options
106
+ #
107
+ # <tt>:login_good</tt> is the url to go to after the authentication
108
+ # process has completed.
109
+ #
110
+ # <tt>:login_fail</tt> is the url to go to after the authentication
111
+ # process has failed.
112
+ #
113
+ # <tt>:login_quit</tt> is the url to go to after the authentication
114
+ # process
115
+ # has been cancelled.
116
+ #
117
+ # === Response options
118
+ #
119
+ # <tt>:no_session</tt> should be a rack response to be returned if no or
120
+ # an incompatible session is found.
121
+ #
122
+ # <tt>:auth_fail</tt> should be a rack response to be returned if an
123
+ # OpenID::DiscoveryFailure occurs. This is typically due to being unable
124
+ # to access the identity url or identity server.
125
+ #
126
+ # <tt>:error</tt> should be a rack response to return if any other
127
+ # generic error would occur and <tt>options[:catch_errors]</tt> is true.
128
+ #
129
+ # === Extensions
130
+ #
131
+ # <tt>:extensions</tt> should be a hash of openid extension
132
+ # implementations. The key should be the extension main module, the value
133
+ # should be an array of arguments for extension::Request.new
134
+ #
135
+ # The hash is iterated over and passed to #add_extension for processing.
136
+ # Please see #add_extension for further documentation.
137
+ def initialize(realm, options={})
138
+ @realm = realm
139
+ realm = URI(realm)
140
+ if realm.path.empty?
141
+ raise ArgumentError, "Invalid realm path: '#{realm.path}'"
142
+ elsif not realm.absolute?
143
+ raise ArgumentError, "Realm '#{@realm}' not absolute"
144
+ end
145
+
146
+ [:return_to, :login_good, :login_fail, :login_quit].each do |key|
147
+ if options.key? key and luri = URI(options[key])
148
+ if !luri.absolute?
149
+ raise ArgumentError, ":#{key} is not an absolute uri: '#{luri}'"
150
+ end
151
+ end
152
+ end
153
+
154
+ if options[:return_to] and ruri = URI(options[:return_to])
155
+ if ruri.path.empty?
156
+ raise ArgumentError, "Invalid return_to path: '#{ruri.path}'"
157
+ elsif realm.path != ruri.path[0, realm.path.size]
158
+ raise ArgumentError, 'return_to not within realm.' \
159
+ end
160
+ end
161
+
162
+ # TODO: extension support
163
+ if extensions = options.delete(:extensions)
164
+ extensions.each do |ext, args|
165
+ add_extension ext, *args
166
+ end
167
+ end
168
+
169
+ @options = {
170
+ :session_key => 'rack.session',
171
+ :openid_param => 'openid_identifier',
172
+ #:return_to, :login_good, :login_fail, :login_quit
173
+ #:no_session, :auth_fail, :error
174
+ :store => OIDStore,
175
+ :immediate => false,
176
+ :anonymous => false,
177
+ :catch_errors => false
178
+ }.merge(options)
179
+ @extensions = {}
180
+ end
181
+
182
+ attr_reader :options, :extensions
183
+
184
+ # It sets up and uses session data at <tt>:openid</tt> within the
185
+ # session. It sets up the ::OpenID::Consumer using the store specified by
186
+ # <tt>options[:store]</tt>.
187
+ #
188
+ # If the parameter specified by <tt>options[:openid_param]</tt> is
189
+ # present, processing is passed to #check and the result is returned.
190
+ #
191
+ # If the parameter 'openid.mode' is set, implying a followup from the
192
+ # openid server, processing is passed to #finish and the result is
193
+ # returned.
194
+ #
195
+ # If neither of these conditions are met, a 400 error is returned.
196
+ #
197
+ # If an error is thrown and <tt>options[:catch_errors]</tt> is false, the
198
+ # exception will be reraised. Otherwise a 500 error is returned.
199
+ def call(env)
200
+ env['rack.auth.openid'] = self
201
+ session = env[@options[:session_key]]
202
+ unless session and session.is_a? Hash
203
+ raise(NoSession, 'No compatible session')
204
+ end
205
+ # let us work in our own namespace...
206
+ session = (session[:openid] ||= {})
207
+ unless session and session.is_a? Hash
208
+ raise(NoSession, 'Incompatible session')
209
+ end
210
+
211
+ request = Rack::Request.new env
212
+ consumer = ::OpenID::Consumer.new session, @options[:store]
213
+
214
+ if request.params['openid.mode']
215
+ finish consumer, session, request
216
+ elsif request.params[@options[:openid_param]]
217
+ check consumer, session, request
218
+ else
219
+ env['rack.errors'].puts "No valid params provided."
220
+ bad_request
221
+ end
222
+ rescue NoSession
223
+ env['rack.errors'].puts($!.message, *$@)
224
+
225
+ @options. ### Missing or incompatible session
226
+ fetch :no_session, [ 500,
227
+ {'Content-Type'=>'text/plain'},
228
+ $!.message ]
229
+ rescue
230
+ env['rack.errors'].puts($!.message, *$@)
231
+
232
+ if not @options[:catch_error]
233
+ raise($!)
234
+ end
235
+ @options.
236
+ fetch :error, [ 500,
237
+ {'Content-Type'=>'text/plain'},
238
+ 'OpenID has encountered an error.' ]
239
+ end
240
+
241
+ # As the first part of OpenID consumer action, #check retrieves the data
242
+ # required for completion.
243
+ #
244
+ # * <tt>session[:openid][:openid_param]</tt> is set to the submitted
245
+ # identifier to be authenticated.
246
+ # * <tt>session[:openid][:site_return]</tt> is set as the request's
247
+ # HTTP_REFERER, unless already set.
248
+ # * <tt>env['rack.auth.openid.request']</tt> is the openid checkid
249
+ # request instance.
250
+ def check(consumer, session, req)
251
+ session[:openid_param] = req.params[@options[:openid_param]]
252
+ oid = consumer.begin(session[:openid_param], @options[:anonymous])
253
+ pp oid if $DEBUG
254
+ req.env['rack.auth.openid.request'] = oid
255
+
256
+ session[:site_return] ||= req.env['HTTP_REFERER']
257
+
258
+ # SETUP_NEEDED check!
259
+ # see OpenID::Consumer::CheckIDRequest docs
260
+ query_args = [@realm, *@options.values_at(:return_to, :immediate)]
261
+ query_args[1] ||= req.url
262
+ query_args[2] = false if session.key? :setup_needed
263
+ pp query_args if $DEBUG
264
+
265
+ ## Extension support
266
+ extensions.each do |ext,args|
267
+ oid.add_extension ext::Request.new(*args)
268
+ end
269
+
270
+ if oid.send_redirect?(*query_args)
271
+ redirect = oid.redirect_url(*query_args)
272
+ if $DEBUG
273
+ pp redirect
274
+ pp Rack::Utils.parse_query(URI(redirect).query)
275
+ end
276
+ [ 303, {'Location'=>redirect}, [] ]
277
+ else
278
+ # check on 'action' option.
279
+ formbody = oid.form_markup(*query_args)
280
+ if $DEBUG
281
+ pp formbody
282
+ end
283
+ body = HTML % ['Confirm...', formbody]
284
+ [ 200, {'Content-Type'=>'text/html'}, body.to_a ]
285
+ end
286
+ rescue ::OpenID::DiscoveryFailure => e
287
+ # thrown from inside OpenID::Consumer#begin by yadis stuff
288
+ req.env['rack.errors'].puts($!.message, *$@)
289
+
290
+ @options. ### Foreign server failed
291
+ fetch :auth_fail, [ 503,
292
+ {'Content-Type'=>'text/plain'},
293
+ 'Foreign server failure.' ]
294
+ end
295
+
296
+ # This is the final portion of authentication. Unless any errors outside
297
+ # of specification occur, a 303 redirect will be returned with Location
298
+ # determined by the OpenID response type. If none of the response type
299
+ # :login_* urls are set, the redirect will be set to
300
+ # <tt>session[:openid][:site_return]</tt>. If
301
+ # <tt>session[:openid][:site_return]</tt> is unset, the realm will be
302
+ # used.
303
+ #
304
+ # Any messages from OpenID's response are appended to the 303 response
305
+ # body.
306
+ #
307
+ # Data gathered from extensions are stored in session[:openid] with the
308
+ # extension's namespace uri as the key.
309
+ #
310
+ # * <tt>env['rack.auth.openid.response']</tt> is the openid response.
311
+ #
312
+ # The four valid possible outcomes are:
313
+ # * failure: <tt>options[:login_fail]</tt> or
314
+ # <tt>session[:site_return]</tt> or the realm
315
+ # * <tt>session[:openid]</tt> is cleared and any messages are send to
316
+ # rack.errors
317
+ # * <tt>session[:openid]['authenticated']</tt> is <tt>false</tt>
318
+ # * success: <tt>options[:login_good]</tt> or
319
+ # <tt>session[:site_return]</tt> or the realm
320
+ # * <tt>session[:openid]</tt> is cleared
321
+ # * <tt>session[:openid]['authenticated']</tt> is <tt>true</tt>
322
+ # * <tt>session[:openid]['identity']</tt> is the actual identifier
323
+ # * <tt>session[:openid]['identifier']</tt> is the pretty identifier
324
+ # * cancel: <tt>options[:login_good]</tt> or
325
+ # <tt>session[:site_return]</tt> or the realm
326
+ # * <tt>session[:openid]</tt> is cleared
327
+ # * <tt>session[:openid]['authenticated']</tt> is <tt>false</tt>
328
+ # * setup_needed: resubmits the authentication request. A flag is set for
329
+ # non-immediate handling.
330
+ # * <tt>session[:openid][:setup_needed]</tt> is set to <tt>true</tt>,
331
+ # which will prevent immediate style openid authentication.
332
+ def finish(consumer, session, req)
333
+ oid = consumer.complete(req.params, req.url)
334
+ pp oid if $DEBUG
335
+ req.env['rack.auth.openid.response'] = oid
336
+
337
+ goto = session.fetch :site_return, @realm
338
+ body = []
339
+
340
+ case oid.status
341
+ when ::OpenID::Consumer::FAILURE
342
+ session.clear
343
+ session['authenticated'] = false
344
+ req.env['rack.errors'].puts oid.message
345
+
346
+ goto = @options[:login_fail] if @option.key? :login_fail
347
+ body << "Authentication unsuccessful.\n"
348
+ when ::OpenID::Consumer::SUCCESS
349
+ session.clear
350
+
351
+ ## Extension support
352
+ extensions.each do |ext, args|
353
+ session[ext::NS_URI] = ext::Response.
354
+ from_success_response(oid).
355
+ get_extension_args
356
+ end
357
+
358
+ session['authenticated'] = true
359
+ # Value for unique identification and such
360
+ session['identity'] = oid.identity_url
361
+ # Value for display and UI labels
362
+ session['identifier'] = oid.display_identifier
363
+
364
+ goto = @options[:login_good] if @options.key? :login_good
365
+ body << "Authentication successful.\n"
366
+ when ::OpenID::Consumer::CANCEL
367
+ session.clear
368
+ session['authenticated'] = false
369
+
370
+ goto = @options[:login_fail] if @option.key? :login_fail
371
+ body << "Authentication cancelled.\n"
372
+ when ::OpenID::Consumer::SETUP_NEEDED
373
+ session[:setup_needed] = true
374
+ unless o_id = session[:openid_param]
375
+ raise('Required values missing.')
376
+ end
377
+
378
+ goto = req.script_name+
379
+ '?'+@options[:openid_param]+
380
+ '='+o_id
381
+ body << "Reauthentication required.\n"
382
+ end
383
+ body << oid.message if oid.message
384
+ [ 303, {'Location'=>goto}, body]
385
+ end
386
+
387
+ # The first argument should be the main extension module.
388
+ # The extension module should contain the constants:
389
+ # * class Request, with OpenID::Extension as an ancestor
390
+ # * class Response, with OpenID::Extension as an ancestor
391
+ # * string NS_URI, which defines the namespace of the extension, should
392
+ # be an absolute http uri
393
+ #
394
+ # All trailing arguments will be passed to extension::Request.new in
395
+ # #check.
396
+ # The openid response will be passed to
397
+ # extension::Response#from_success_response, #get_extension_args will be
398
+ # called on the result to attain the gathered data.
399
+ #
400
+ # This method returns the key at which the response data will be found in
401
+ # the session, which is the namespace uri by default.
402
+ def add_extension ext, *args
403
+ if not ext.is_a? Module
404
+ raise TypeError, "#{ext.inspect} is not a module"
405
+ elsif not (m = %w'Request Response NS_URI' - ext.constants).empty?
406
+ raise ArgumentError, "#{ext.inspect} missing #{m*', '}"
407
+ end
408
+
409
+ consts = [ext::Request, ext::Response]
410
+
411
+ if not consts.all?{|c| c.is_a? Class }
412
+ raise TypeError, "#{ext.inspect}'s Request or Response is not a class"
413
+ elsif not consts.all?{|c| ::OpenID::Extension > c }
414
+ raise ArgumentError, "#{ext.inspect}'s Request or Response not a decendant of OpenID::Extension"
415
+ end
416
+
417
+ if not ext::NS_URI.is_a? String
418
+ raise TypeError, "#{ext.inspect}'s NS_URI is not a string"
419
+ elsif not uri = URI(ext::NS_URI)
420
+ raise ArgumentError, "#{ext.inspect}'s NS_URI is not a valid uri"
421
+ elsif not uri.scheme =~ /^https?$/
422
+ raise ArgumentError, "#{ext.inspect}'s NS_URI is not an http uri"
423
+ elsif not uri.absolute?
424
+ raise ArgumentError, "#{ext.inspect}'s NS_URI is not and absolute uri"
425
+ end
426
+ @extensions[ext] = args
427
+ return ext::NS_URI
428
+ end
429
+
430
+ # A conveniance method that returns the namespace of all current
431
+ # extensions used by this instance.
432
+ def extension_namespaces
433
+ @extensions.keys.map{|e|e::NS_URI}
434
+ end
435
+ end
436
+ end
437
+ end