rack 0.9.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (79) hide show
  1. data/COPYING +1 -1
  2. data/RDOX +115 -16
  3. data/README +54 -7
  4. data/Rakefile +61 -85
  5. data/SPEC +50 -17
  6. data/bin/rackup +9 -5
  7. data/example/protectedlobster.ru +1 -1
  8. data/lib/rack.rb +7 -3
  9. data/lib/rack/auth/abstract/handler.rb +13 -4
  10. data/lib/rack/auth/digest/md5.rb +1 -1
  11. data/lib/rack/auth/digest/request.rb +2 -2
  12. data/lib/rack/auth/openid.rb +344 -302
  13. data/lib/rack/builder.rb +1 -5
  14. data/lib/rack/chunked.rb +49 -0
  15. data/lib/rack/conditionalget.rb +4 -0
  16. data/lib/rack/content_length.rb +7 -3
  17. data/lib/rack/content_type.rb +23 -0
  18. data/lib/rack/deflater.rb +83 -74
  19. data/lib/rack/directory.rb +5 -2
  20. data/lib/rack/file.rb +4 -1
  21. data/lib/rack/handler.rb +22 -1
  22. data/lib/rack/handler/cgi.rb +7 -3
  23. data/lib/rack/handler/fastcgi.rb +26 -24
  24. data/lib/rack/handler/lsws.rb +7 -4
  25. data/lib/rack/handler/mongrel.rb +5 -3
  26. data/lib/rack/handler/scgi.rb +5 -3
  27. data/lib/rack/handler/thin.rb +3 -0
  28. data/lib/rack/handler/webrick.rb +11 -5
  29. data/lib/rack/lint.rb +138 -66
  30. data/lib/rack/lock.rb +16 -0
  31. data/lib/rack/mime.rb +4 -4
  32. data/lib/rack/mock.rb +3 -3
  33. data/lib/rack/reloader.rb +88 -46
  34. data/lib/rack/request.rb +46 -10
  35. data/lib/rack/response.rb +15 -3
  36. data/lib/rack/rewindable_input.rb +98 -0
  37. data/lib/rack/session/abstract/id.rb +71 -82
  38. data/lib/rack/session/cookie.rb +2 -0
  39. data/lib/rack/session/memcache.rb +59 -47
  40. data/lib/rack/session/pool.rb +56 -29
  41. data/lib/rack/showexceptions.rb +2 -1
  42. data/lib/rack/showstatus.rb +1 -1
  43. data/lib/rack/urlmap.rb +12 -5
  44. data/lib/rack/utils.rb +115 -65
  45. data/rack.gemspec +54 -0
  46. data/test/multipart/binary +0 -0
  47. data/test/multipart/empty +10 -0
  48. data/test/multipart/ie +6 -0
  49. data/test/multipart/nested +10 -0
  50. data/test/multipart/none +9 -0
  51. data/test/multipart/text +10 -0
  52. data/test/spec_rack_auth_basic.rb +5 -1
  53. data/test/spec_rack_auth_digest.rb +93 -36
  54. data/test/spec_rack_auth_openid.rb +47 -100
  55. data/test/spec_rack_builder.rb +2 -2
  56. data/test/spec_rack_chunked.rb +62 -0
  57. data/test/spec_rack_conditionalget.rb +7 -7
  58. data/test/spec_rack_content_type.rb +30 -0
  59. data/test/spec_rack_deflater.rb +36 -14
  60. data/test/spec_rack_directory.rb +1 -1
  61. data/test/spec_rack_file.rb +11 -0
  62. data/test/spec_rack_handler.rb +21 -2
  63. data/test/spec_rack_lint.rb +163 -44
  64. data/test/spec_rack_lock.rb +38 -0
  65. data/test/spec_rack_mock.rb +6 -1
  66. data/test/spec_rack_request.rb +81 -12
  67. data/test/spec_rack_response.rb +46 -2
  68. data/test/spec_rack_rewindable_input.rb +118 -0
  69. data/test/spec_rack_session_memcache.rb +170 -62
  70. data/test/spec_rack_session_pool.rb +129 -41
  71. data/test/spec_rack_static.rb +2 -2
  72. data/test/spec_rack_thin.rb +3 -2
  73. data/test/spec_rack_urlmap.rb +10 -0
  74. data/test/spec_rack_utils.rb +214 -49
  75. data/test/spec_rack_webrick.rb +7 -0
  76. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  77. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  78. metadata +95 -6
  79. data/AUTHORS +0 -8
data/SPEC CHANGED
@@ -34,7 +34,9 @@ below.
34
34
  within the application. This may be an
35
35
  empty string, if the request URL targets
36
36
  the application root and does not have a
37
- trailing slash.
37
+ trailing slash. This value may be
38
+ percent-encoded when I originating from
39
+ a URL.
38
40
  <tt>QUERY_STRING</tt>:: The portion of the request URL that
39
41
  follows the <tt>?</tt>, if any. May be
40
42
  empty, but is always required!
@@ -50,18 +52,27 @@ below.
50
52
  request.
51
53
  In addition to this, the Rack environment must include these
52
54
  Rack-specific variables:
53
- <tt>rack.version</tt>:: The Array [0,1], representing this version of Rack.
55
+ <tt>rack.version</tt>:: The Array [1,0], representing this version of Rack.
54
56
  <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL.
55
57
  <tt>rack.input</tt>:: See below, the input stream.
56
58
  <tt>rack.errors</tt>:: See below, the error stream.
57
59
  <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
58
60
  <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
59
61
  <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar).
62
+ Additional environment specifications have approved to
63
+ standardized middleware APIs. None of these are required to
64
+ be implemented by the server.
65
+ <tt>rack.session</tt>:: A hash like interface for storing request session data.
66
+ The store must implement:
67
+ store(key, value) (aliased as []=);
68
+ fetch(key, default = nil) (aliased as []);
69
+ delete(key);
70
+ clear;
60
71
  The server or the application can store their own data in the
61
72
  environment, too. The keys must contain at least one dot,
62
73
  and should be prefixed uniquely. The prefix <tt>rack.</tt>
63
- is reserved for use with the Rack core distribution and must
64
- not be used otherwise.
74
+ is reserved for use with the Rack core distribution and other
75
+ accepted specifications and must not be used otherwise.
65
76
  The environment must not contain the keys
66
77
  <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
67
78
  (use the versions without <tt>HTTP_</tt>).
@@ -80,12 +91,26 @@ There are the following restrictions:
80
91
  <tt>SCRIPT_NAME</tt> is empty.
81
92
  <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
82
93
  === The Input Stream
83
- The input stream must respond to +gets+, +each+ and +read+.
94
+ The input stream is an IO-like object which contains the raw HTTP
95
+ POST data. If it is a file then it must be opened in binary mode.
96
+ The input stream must respond to +gets+, +each+, +read+ and +rewind+.
84
97
  * +gets+ must be called without arguments and return a string,
85
98
  or +nil+ on EOF.
86
- * +read+ must be called without or with one integer argument
87
- and return a string, or +nil+ on EOF.
99
+ * +read+ behaves like IO#read. Its signature is <tt>read([length, [buffer]])</tt>.
100
+ If given, +length+ must be an non-negative Integer (>= 0) or +nil+, and +buffer+ must
101
+ be a String and may not be nil. If +length+ is given and not nil, then this method
102
+ reads at most +length+ bytes from the input stream. If +length+ is not given or nil,
103
+ then this method reads all data until EOF.
104
+ When EOF is reached, this method returns nil if +length+ is given and not nil, or ""
105
+ if +length+ is not given or is nil.
106
+ If +buffer+ is given, then the read data will be placed into +buffer+ instead of a
107
+ newly created String object.
88
108
  * +each+ must be called without arguments and only yield Strings.
109
+ * +rewind+ must be called without arguments. It rewinds the input
110
+ stream back to the beginning. It must not raise Errno::ESPIPE:
111
+ that is, it may not be a pipe or a socket. Therefore, handler
112
+ developers must buffer the input data into some rewindable object
113
+ if the underlying input stream is not rewindable.
89
114
  * +close+ must never be called on the input stream.
90
115
  === The Error Stream
91
116
  The error stream must respond to +puts+, +write+ and +flush+.
@@ -96,30 +121,38 @@ The error stream must respond to +puts+, +write+ and +flush+.
96
121
  * +close+ must never be called on the error stream.
97
122
  == The Response
98
123
  === The Status
99
- The status, if parsed as integer (+to_i+), must be greater than or equal to 100.
124
+ This is an HTTP status. When parsed as integer (+to_i+), it must be
125
+ greater than or equal to 100.
100
126
  === The Headers
101
- The header must respond to each, and yield values of key and value.
127
+ The header must respond to +each+, and yield values of key and value.
102
128
  The header keys must be Strings.
103
129
  The header must not contain a +Status+ key,
104
130
  contain keys with <tt>:</tt> or newlines in their name,
105
131
  contain keys names that end in <tt>-</tt> or <tt>_</tt>,
106
132
  but only contain keys that consist of
107
133
  letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter.
108
- The values of the header must respond to #each.
109
- The values passed on #each must be Strings
110
- and not contain characters below 037.
134
+ The values of the header must be Strings,
135
+ consisting of lines (for multiple header values, e.g. multiple
136
+ <tt>Set-Cookie</tt> values) seperated by "\n".
137
+ The lines must not contain characters below 037.
111
138
  === The Content-Type
112
139
  There must be a <tt>Content-Type</tt>, except when the
113
140
  +Status+ is 1xx, 204 or 304, in which case there must be none
114
141
  given.
115
142
  === The Content-Length
116
- There must be a <tt>Content-Length</tt>, except when the
117
- +Status+ is 1xx, 204 or 304, in which case there must be none
118
- given.
143
+ There must not be a <tt>Content-Length</tt> header when the
144
+ +Status+ is 1xx, 204 or 304.
119
145
  === The Body
120
- The Body must respond to #each
146
+ The Body must respond to +each+
121
147
  and must only yield String values.
122
- If the Body responds to #close, it will be called after iteration.
148
+ The Body itself should not be an instance of String, as this will
149
+ break in Ruby 1.9.
150
+ If the Body responds to +close+, it will be called after iteration.
151
+ If the Body responds to +to_path+, it must return a String
152
+ identifying the location of a file whose contents are identical
153
+ to that produced by calling +each+; this may be used by the
154
+ server as an alternative, possibly more efficient way to
155
+ transport the response.
123
156
  The Body commonly is an Array of Strings, the application
124
157
  instance itself, or a File-like object.
125
158
  == Thanks
data/bin/rackup CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # -*- ruby -*-
3
3
 
4
+ $LOAD_PATH.unshift File.expand_path("#{__FILE__}/../../lib")
5
+ autoload :Rack, 'rack'
6
+
4
7
  require 'optparse'
5
8
 
6
9
  automatic = false
@@ -10,6 +13,10 @@ daemonize = false
10
13
  pid = nil
11
14
  options = {:Port => 9292, :Host => "0.0.0.0", :AccessLog => []}
12
15
 
16
+ # Don't evaluate CGI ISINDEX parameters.
17
+ # http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
18
+ ARGV.clear if ENV.include?("REQUEST_METHOD")
19
+
13
20
  opts = OptionParser.new("", 24, ' ') { |opts|
14
21
  opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]"
15
22
 
@@ -74,7 +81,6 @@ opts = OptionParser.new("", 24, ' ') { |opts|
74
81
  end
75
82
 
76
83
  opts.on_tail("--version", "Show version") do
77
- require 'rack'
78
84
  puts "Rack #{Rack.version}"
79
85
  exit
80
86
  end
@@ -94,11 +100,9 @@ if config =~ /\.ru$/
94
100
  if cfgfile[/^#\\(.*)/]
95
101
  opts.parse! $1.split(/\s+/)
96
102
  end
97
- require 'rack'
98
103
  inner_app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app",
99
104
  nil, config
100
105
  else
101
- require 'rack'
102
106
  require config
103
107
  inner_app = Object.const_get(File.basename(config, '.rb').capitalize)
104
108
  end
@@ -127,7 +131,7 @@ p server if $DEBUG
127
131
  case env
128
132
  when "development"
129
133
  app = Rack::Builder.new {
130
- use Rack::CommonLogger, STDERR unless server.name =~ /CGI/
134
+ use Rack::CommonLogger, $stderr unless server.name =~ /CGI/
131
135
  use Rack::ShowExceptions
132
136
  use Rack::Lint
133
137
  run inner_app
@@ -135,7 +139,7 @@ when "development"
135
139
 
136
140
  when "deployment"
137
141
  app = Rack::Builder.new {
138
- use Rack::CommonLogger, STDERR unless server.name =~ /CGI/
142
+ use Rack::CommonLogger, $stderr unless server.name =~ /CGI/
139
143
  run inner_app
140
144
  }.to_app
141
145
 
@@ -1,7 +1,7 @@
1
1
  require 'rack/lobster'
2
2
 
3
3
  use Rack::ShowExceptions
4
- use Rack::Auth::Basic do |username, password|
4
+ use Rack::Auth::Basic, "Lobster 2.0" do |username, password|
5
5
  'secret' == password
6
6
  end
7
7
 
@@ -3,7 +3,8 @@
3
3
  # Rack is freely distributable under the terms of an MIT-style license.
4
4
  # See COPYING or http://www.opensource.org/licenses/mit-license.php.
5
5
 
6
- $: << File.expand_path(File.dirname(__FILE__))
6
+ path = File.expand_path(File.dirname(__FILE__))
7
+ $:.unshift(path) unless $:.include?(path)
7
8
 
8
9
 
9
10
  # The Rack main module, serving as a namespace for all core Rack
@@ -14,7 +15,7 @@ $: << File.expand_path(File.dirname(__FILE__))
14
15
 
15
16
  module Rack
16
17
  # The Rack protocol version number implemented.
17
- VERSION = [0,1]
18
+ VERSION = [1,0]
18
19
 
19
20
  # Return the Rack protocol version as a dotted string.
20
21
  def self.version
@@ -23,14 +24,16 @@ module Rack
23
24
 
24
25
  # Return the Rack release as a dotted string.
25
26
  def self.release
26
- "0.9"
27
+ "1.0"
27
28
  end
28
29
 
29
30
  autoload :Builder, "rack/builder"
30
31
  autoload :Cascade, "rack/cascade"
32
+ autoload :Chunked, "rack/chunked"
31
33
  autoload :CommonLogger, "rack/commonlogger"
32
34
  autoload :ConditionalGet, "rack/conditionalget"
33
35
  autoload :ContentLength, "rack/content_length"
36
+ autoload :ContentType, "rack/content_type"
34
37
  autoload :File, "rack/file"
35
38
  autoload :Deflater, "rack/deflater"
36
39
  autoload :Directory, "rack/directory"
@@ -38,6 +41,7 @@ module Rack
38
41
  autoload :Handler, "rack/handler"
39
42
  autoload :Head, "rack/head"
40
43
  autoload :Lint, "rack/lint"
44
+ autoload :Lock, "rack/lock"
41
45
  autoload :MethodOverride, "rack/methodoverride"
42
46
  autoload :Mime, "rack/mime"
43
47
  autoload :Recursive, "rack/recursive"
@@ -8,19 +8,28 @@ module Rack
8
8
 
9
9
  attr_accessor :realm
10
10
 
11
- def initialize(app, &authenticator)
12
- @app, @authenticator = app, authenticator
11
+ def initialize(app, realm=nil, &authenticator)
12
+ @app, @realm, @authenticator = app, realm, authenticator
13
13
  end
14
14
 
15
15
 
16
16
  private
17
17
 
18
18
  def unauthorized(www_authenticate = challenge)
19
- return [ 401, { 'WWW-Authenticate' => www_authenticate.to_s }, [] ]
19
+ return [ 401,
20
+ { 'Content-Type' => 'text/plain',
21
+ 'Content-Length' => '0',
22
+ 'WWW-Authenticate' => www_authenticate.to_s },
23
+ []
24
+ ]
20
25
  end
21
26
 
22
27
  def bad_request
23
- [ 400, {}, [] ]
28
+ return [ 400,
29
+ { 'Content-Type' => 'text/plain',
30
+ 'Content-Length' => '0' },
31
+ []
32
+ ]
24
33
  end
25
34
 
26
35
  end
@@ -21,7 +21,7 @@ module Rack
21
21
 
22
22
  attr_writer :passwords_hashed
23
23
 
24
- def initialize(app)
24
+ def initialize(*args)
25
25
  super
26
26
  @passwords_hashed = nil
27
27
  end
@@ -8,7 +8,7 @@ module Rack
8
8
  class Request < Auth::AbstractRequest
9
9
 
10
10
  def method
11
- @env['REQUEST_METHOD']
11
+ @env['rack.methodoverride.original_method'] || @env['REQUEST_METHOD']
12
12
  end
13
13
 
14
14
  def digest?
@@ -16,7 +16,7 @@ module Rack
16
16
  end
17
17
 
18
18
  def correct_uri?
19
- @env['PATH_INFO'] == uri
19
+ (@env['SCRIPT_NAME'].to_s + @env['PATH_INFO'].to_s) == uri
20
20
  end
21
21
 
22
22
  def nonce
@@ -1,18 +1,30 @@
1
1
  # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
2
2
 
3
3
  gem 'ruby-openid', '~> 2' if defined? Gem
4
- require 'rack/auth/abstract/handler' #rack
5
- require 'uri' #std
6
- require 'pp' #std
4
+ require 'rack/request'
5
+ require 'rack/utils'
6
+ require 'rack/auth/abstract/handler'
7
+ require 'uri'
7
8
  require 'openid' #gem
8
9
  require 'openid/extension' #gem
9
10
  require 'openid/store/memory' #gem
10
11
 
11
12
  module Rack
13
+ class Request
14
+ def openid_request
15
+ @env['rack.auth.openid.request']
16
+ end
17
+
18
+ def openid_response
19
+ @env['rack.auth.openid.response']
20
+ end
21
+ end
22
+
12
23
  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.
24
+
25
+ # Rack::Auth::OpenID provides a simple method for setting up an OpenID
26
+ # Consumer. It requires the ruby-openid library from janrain to operate,
27
+ # as well as a rack method of session management.
16
28
  #
17
29
  # The ruby-openid home page is at http://openidenabled.com/ruby-openid/.
18
30
  #
@@ -26,61 +38,26 @@ module Rack
26
38
  # It is recommended to read through the OpenID spec, as well as
27
39
  # ruby-openid's documentation, to understand what exactly goes on. However
28
40
  # a setup as simple as the presented examples is enough to provide
29
- # functionality.
41
+ # Consumer functionality.
30
42
  #
31
43
  # 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.
44
+ # ruby-openid library, which provides OpenID 1.0 compatiblity.
41
45
  #
42
46
  # NOTE: Due to the amount of data that this library stores in the
43
47
  # session, Rack::Session::Cookie may fault.
44
- class OpenID < AbstractHandler
48
+
49
+ class OpenID
50
+
45
51
  class NoSession < RuntimeError; end
52
+ class BadExtension < RuntimeError; end
46
53
  # Required for ruby-openid
47
- OIDStore = ::OpenID::Store::Memory.new
48
- HTML = '<html><head><title>%s</title></head><body>%s</body></html>'
54
+ ValidStatus = [:success, :setup_needed, :cancel, :failure]
49
55
 
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
56
  # = Arguments
75
57
  #
76
58
  # 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.
59
+ # with their identity. This is required, also treated as the trust_root
60
+ # in OpenID 1.x exchanges.
84
61
  #
85
62
  # The optional second argument is a hash of options.
86
63
  #
@@ -89,8 +66,8 @@ module Rack
89
66
  # <tt>:return_to</tt> defines the url to return to after the client
90
67
  # authenticates with the openid service provider. This url should point
91
68
  # 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.
69
+ # provided, return_to will be the current url which allows flexibility
70
+ # with caveats.
94
71
  #
95
72
  # <tt>:session_key</tt> defines the key to the session hash in the env.
96
73
  # It defaults to 'rack.session'.
@@ -99,297 +76,229 @@ module Rack
99
76
  # find the identifier to resolve. As per the 2.0 spec, the default is
100
77
  # 'openid_identifier'.
101
78
  #
102
- # <tt>:immediate</tt> as true will make immediate type of requests the
103
- # default. See OpenID specification documentation.
79
+ # <tt>:store</tt> defined what OpenID Store to use for persistant
80
+ # information. By default a Store::Memory will be used.
104
81
  #
105
- # === URL options
82
+ # <tt>:immediate</tt> as true will make initial requests to be of an
83
+ # immediate type. This is false by default. See OpenID specification
84
+ # documentation.
106
85
  #
107
- # <tt>:login_good</tt> is the url to go to after the authentication
108
- # process has completed.
86
+ # <tt>:extensions</tt> should be a hash of openid extension
87
+ # implementations. The key should be the extension main module, the value
88
+ # should be an array of arguments for extension::Request.new.
89
+ # The hash is iterated over and passed to #add_extension for processing.
90
+ # Please see #add_extension for further documentation.
109
91
  #
110
- # <tt>:login_fail</tt> is the url to go to after the authentication
111
- # process has failed.
92
+ # == Examples
112
93
  #
113
- # <tt>:login_quit</tt> is the url to go to after the authentication
114
- # process
115
- # has been cancelled.
94
+ # simple_oid = OpenID.new('http://mysite.com/')
116
95
  #
117
- # === Response options
96
+ # return_oid = OpenID.new('http://mysite.com/', {
97
+ # :return_to => 'http://mysite.com/openid'
98
+ # })
118
99
  #
119
- # <tt>:no_session</tt> should be a rack response to be returned if no or
120
- # an incompatible session is found.
100
+ # complex_oid = OpenID.new('http://mysite.com/',
101
+ # :immediate => true,
102
+ # :extensions => {
103
+ # ::OpenID::SReg => [['email'],['nickname']]
104
+ # }
105
+ # )
121
106
  #
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.
107
+ # = Advanced
125
108
  #
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.
109
+ # Most of the functionality of this library is encapsulated such that
110
+ # expansion and overriding functions isn't difficult nor tricky.
111
+ # Alternately, to avoid opening up singleton objects or subclassing, a
112
+ # wrapper rack middleware can be composed to act upon Auth::OpenID's
113
+ # responses. See #check and #finish for locations of pertinent data.
128
114
  #
129
- # === Extensions
115
+ # == Responses
130
116
  #
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
117
+ # To change the responses that Auth::OpenID returns, override the methods
118
+ # #redirect, #bad_request, #unauthorized, #access_denied, and
119
+ # #foreign_server_failure.
134
120
  #
135
- # The hash is iterated over and passed to #add_extension for processing.
136
- # Please see #add_extension for further documentation.
121
+ # Additionally #confirm_post_params is used when the URI would exceed
122
+ # length limits on a GET request when doing the initial verification
123
+ # request.
124
+ #
125
+ # == Processing
126
+ #
127
+ # To change methods of processing completed transactions, override the
128
+ # methods #success, #setup_needed, #cancel, and #failure. Please ensure
129
+ # the returned object is a rack compatible response.
130
+ #
131
+ # The first argument is an OpenID::Response, the second is a
132
+ # Rack::Request of the current request, the last is the hash used in
133
+ # ruby-openid handling, which can be found manually at
134
+ # env['rack.session'][:openid].
135
+ #
136
+ # This is useful if you wanted to expand the processing done, such as
137
+ # setting up user accounts.
138
+ #
139
+ # oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to
140
+ # def oid_app.success oid, request, session
141
+ # user = Models::User[oid.identity_url]
142
+ # user ||= Models::User.create_from_openid oid
143
+ # request['rack.session'][:user] = user.id
144
+ # redirect MyApp.site_home
145
+ # end
146
+ #
147
+ # site_map['/openid'] = oid_app
148
+ # map = Rack::URLMap.new site_map
149
+ # ...
150
+
137
151
  def initialize(realm, options={})
138
- @realm = realm
139
152
  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"
153
+ raise ArgumentError, "Invalid realm: #{realm}" \
154
+ unless realm.absolute? \
155
+ and realm.fragment.nil? \
156
+ and realm.scheme =~ /^https?$/ \
157
+ and realm.host =~ /^(\*\.)?#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+/
158
+ realm.path = '/' if realm.path.empty?
159
+ @realm = realm.to_s
160
+
161
+ if ruri = options[:return_to]
162
+ ruri = URI(ruri)
163
+ raise ArgumentError, "Invalid return_to: #{ruri}" \
164
+ unless ruri.absolute? \
165
+ and ruri.scheme =~ /^https?$/ \
166
+ and ruri.fragment.nil?
167
+ raise ArgumentError, "return_to #{ruri} not within realm #{realm}" \
168
+ unless self.within_realm?(ruri)
169
+ @return_to = ruri.to_s
144
170
  end
145
171
 
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
172
+ @session_key = options[:session_key] || 'rack.session'
173
+ @openid_param = options[:openid_param] || 'openid_identifier'
174
+ @store = options[:store] || ::OpenID::Store::Memory.new
175
+ @immediate = !!options[:immediate]
161
176
 
162
- # TODO: extension support
177
+ @extensions = {}
163
178
  if extensions = options.delete(:extensions)
164
179
  extensions.each do |ext, args|
165
180
  add_extension ext, *args
166
181
  end
167
182
  end
168
183
 
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 = {}
184
+ # Undocumented, semi-experimental
185
+ @anonymous = !!options[:anonymous]
180
186
  end
181
187
 
182
- attr_reader :options, :extensions
188
+ attr_reader :realm, :return_to, :session_key, :openid_param, :store,
189
+ :immediate, :extensions
183
190
 
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>.
191
+ # Sets up and uses session data at <tt>:openid</tt> within the session.
192
+ # Errors in this setup will raise a NoSession exception.
193
+ #
194
+ # If the parameter 'openid.mode' is set, which implies a followup from
195
+ # the openid server, processing is passed to #finish and the result is
196
+ # returned. However, if there is no appropriate openid information in the
197
+ # session, a 400 error is returned.
187
198
  #
188
199
  # If the parameter specified by <tt>options[:openid_param]</tt> is
189
200
  # present, processing is passed to #check and the result is returned.
190
201
  #
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.
202
+ # If neither of these conditions are met, #unauthorized is called.
203
+
199
204
  def call(env)
200
205
  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')
206
+ env_session = env[@session_key]
207
+ unless env_session and env_session.is_a?(Hash)
208
+ raise NoSession, 'No compatible session'
204
209
  end
205
210
  # let us work in our own namespace...
206
- session = (session[:openid] ||= {})
207
- unless session and session.is_a? Hash
208
- raise(NoSession, 'Incompatible session')
211
+ session = (env_session[:openid] ||= {})
212
+ unless session and session.is_a?(Hash)
213
+ raise NoSession, 'Incompatible openid session'
209
214
  end
210
215
 
211
- request = Rack::Request.new env
212
- consumer = ::OpenID::Consumer.new session, @options[:store]
216
+ request = Rack::Request.new(env)
217
+ consumer = ::OpenID::Consumer.new(session, @store)
213
218
 
214
- if request.params['openid.mode']
215
- finish consumer, session, request
216
- elsif request.params[@options[:openid_param]]
217
- check consumer, session, request
219
+ if mode = request.GET['openid.mode']
220
+ if session.key?(:openid_param)
221
+ finish(consumer, session, request)
222
+ else
223
+ bad_request
224
+ end
225
+ elsif request.GET[@openid_param]
226
+ check(consumer, session, request)
218
227
  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($!)
228
+ unauthorized
234
229
  end
235
- @options.
236
- fetch :error, [ 500,
237
- {'Content-Type'=>'text/plain'},
238
- 'OpenID has encountered an error.' ]
239
230
  end
240
231
 
241
232
  # As the first part of OpenID consumer action, #check retrieves the data
242
233
  # required for completion.
243
234
  #
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.
235
+ # If all parameters fit within the max length of a URI, a 303 redirect
236
+ # will be returned. Otherwise #confirm_post_params will be called.
237
+ #
238
+ # Any messages from OpenID's request are logged to env['rack.errors']
239
+ #
240
+ # <tt>env['rack.auth.openid.request']</tt> is the openid checkid request
241
+ # instance.
242
+ #
243
+ # <tt>session[:openid_param]</tt> is set to the openid identifier
244
+ # provided by the user.
245
+ #
246
+ # <tt>session[:return_to]</tt> is set to the return_to uri given to the
247
+ # identity provider.
248
+
250
249
  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
250
+ oid = consumer.begin(req.GET[@openid_param], @anonymous)
254
251
  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
252
+ req.env['rack.errors'].puts(oid.message)
253
+ p oid if $DEBUG
264
254
 
265
255
  ## Extension support
266
256
  extensions.each do |ext,args|
267
- oid.add_extension ext::Request.new(*args)
257
+ oid.add_extension(ext::Request.new(*args))
268
258
  end
269
259
 
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}, [] ]
260
+ session[:openid_param] = req.GET[openid_param]
261
+ return_to_uri = return_to ? return_to : req.url
262
+ session[:return_to] = return_to_uri
263
+ immediate = session.key?(:setup_needed) ? false : immediate
264
+
265
+ if oid.send_redirect?(realm, return_to_uri, immediate)
266
+ uri = oid.redirect_url(realm, return_to_uri, immediate)
267
+ redirect(uri)
277
268
  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 ]
269
+ confirm_post_params(oid, realm, return_to_uri, immediate)
285
270
  end
286
271
  rescue ::OpenID::DiscoveryFailure => e
287
272
  # 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.' ]
273
+ req.env['rack.errors'].puts([e.message, *e.backtrace]*"\n")
274
+ return foreign_server_failure
294
275
  end
295
276
 
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
- #
277
+ # This is the final portion of authentication.
278
+ # If successful, a redirect to the realm is be returned.
307
279
  # Data gathered from extensions are stored in session[:openid] with the
308
280
  # extension's namespace uri as the key.
309
281
  #
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.
282
+ # Any messages from OpenID's response are logged to env['rack.errors']
283
+ #
284
+ # <tt>env['rack.auth.openid.response']</tt> will contain the openid
285
+ # response.
286
+
332
287
  def finish(consumer, session, req)
333
- oid = consumer.complete(req.params, req.url)
334
- pp oid if $DEBUG
288
+ oid = consumer.complete(req.GET, req.url)
335
289
  req.env['rack.auth.openid.response'] = oid
290
+ req.env['rack.errors'].puts(oid.message)
291
+ p oid if $DEBUG
336
292
 
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 @options.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 @options.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]
293
+ raise unless ValidStatus.include?(oid.status)
294
+ __send__(oid.status, oid, req, session)
385
295
  end
386
296
 
387
297
  # The first argument should be the main extension module.
388
298
  # 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
299
+ # * class Request, should have OpenID::Extension as an ancestor
300
+ # * class Response, should have OpenID::Extension as an ancestor
301
+ # * string NS_URI, which defining the namespace of the extension
393
302
  #
394
303
  # All trailing arguments will be passed to extension::Request.new in
395
304
  # #check.
@@ -399,39 +308,172 @@ module Rack
399
308
  #
400
309
  # This method returns the key at which the response data will be found in
401
310
  # 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 !(m = %w'Request Response NS_URI' -
406
- ext.constants.map{ |c| c.to_s }).empty?
407
- raise ArgumentError, "#{ext.inspect} missing #{m*', '}"
311
+
312
+ def add_extension(ext, *args)
313
+ raise BadExtension unless valid_extension?(ext)
314
+ extensions[ext] = args
315
+ return ext::NS_URI
316
+ end
317
+
318
+ # Checks the validitity, in the context of usage, of a submitted
319
+ # extension.
320
+
321
+ def valid_extension?(ext)
322
+ if not %w[NS_URI Request Response].all?{|c| ext.const_defined?(c) }
323
+ raise ArgumentError, 'Extension is missing constants.'
324
+ elsif not ext::Response.respond_to?(:from_success_response)
325
+ raise ArgumentError, 'Response is missing required method.'
408
326
  end
327
+ return true
328
+ rescue
329
+ return false
330
+ end
331
+
332
+ # Checks the provided uri to ensure it'd be considered within the realm.
333
+ # is currently not compatible with wildcard realms.
334
+
335
+ def within_realm? uri
336
+ uri = URI.parse(uri.to_s)
337
+ realm = URI.parse(self.realm)
338
+ return false unless uri.absolute?
339
+ return false unless uri.path[0, realm.path.size] == realm.path
340
+ return false unless uri.host == realm.host or realm.host[/^\*\./]
341
+ # for wildcard support, is awkward with URI limitations
342
+ realm_match = Regexp.escape(realm.host).
343
+ sub(/^\*\./,"^#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+.")+'$'
344
+ return false unless uri.host.match(realm_match)
345
+ return true
346
+ end
347
+ alias_method :include?, :within_realm?
348
+
349
+ protected
350
+
351
+ ### These methods define some of the boilerplate responses.
409
352
 
410
- consts = [ext::Request, ext::Response]
353
+ # Returns an html form page for posting to an Identity Provider if the
354
+ # GET request would exceed the upper URI length limit.
411
355
 
412
- if not consts.all?{|c| c.is_a? Class }
413
- raise TypeError, "#{ext.inspect}'s Request or Response is not a class"
414
- elsif not consts.all?{|c| ::OpenID::Extension > c }
415
- raise ArgumentError, "#{ext.inspect}'s Request or Response not a decendant of OpenID::Extension"
356
+ def confirm_post_params(oid, realm, return_to, immediate)
357
+ Rack::Response.new.finish do |r|
358
+ r.write '<html><head><title>Confirm...</title></head><body>'
359
+ r.write oid.form_markup(realm, return_to, immediate)
360
+ r.write '</body></html>'
416
361
  end
362
+ end
363
+
364
+ # Returns a 303 redirect with the destination of that provided by the
365
+ # argument.
366
+
367
+ def redirect(uri)
368
+ [ 303, {'Content-Length'=>'0', 'Content-Type'=>'text/plain',
369
+ 'Location' => uri},
370
+ [] ]
371
+ end
372
+
373
+ # Returns an empty 400 response.
417
374
 
418
- if not ext::NS_URI.is_a? String
419
- raise TypeError, "#{ext.inspect}'s NS_URI is not a string"
420
- elsif not uri = URI(ext::NS_URI)
421
- raise ArgumentError, "#{ext.inspect}'s NS_URI is not a valid uri"
422
- elsif not uri.scheme =~ /^https?$/
423
- raise ArgumentError, "#{ext.inspect}'s NS_URI is not an http uri"
424
- elsif not uri.absolute?
425
- raise ArgumentError, "#{ext.inspect}'s NS_URI is not and absolute uri"
375
+ def bad_request
376
+ [ 400, {'Content-Type'=>'text/plain', 'Content-Length'=>'0'},
377
+ [''] ]
378
+ end
379
+
380
+ # Returns a basic unauthorized 401 response.
381
+
382
+ def unauthorized
383
+ [ 401, {'Content-Type' => 'text/plain', 'Content-Length' => '13'},
384
+ ['Unauthorized.'] ]
385
+ end
386
+
387
+ # Returns a basic access denied 403 response.
388
+
389
+ def access_denied
390
+ [ 403, {'Content-Type' => 'text/plain', 'Content-Length' => '14'},
391
+ ['Access denied.'] ]
392
+ end
393
+
394
+ # Returns a 503 response to be used if communication with the remote
395
+ # OpenID server fails.
396
+
397
+ def foreign_server_failure
398
+ [ 503, {'Content-Type'=>'text/plain', 'Content-Length' => '23'},
399
+ ['Foreign server failure.'] ]
400
+ end
401
+
402
+ private
403
+
404
+ ### These methods are called after a transaction is completed, depending
405
+ # on its outcome. These should all return a rack compatible response.
406
+ # You'd want to override these to provide additional functionality.
407
+
408
+ # Called to complete processing on a successful transaction.
409
+ # Within the openid session, :openid_identity and :openid_identifier are
410
+ # set to the user friendly and the standard representation of the
411
+ # validated identity. All other data in the openid session is cleared.
412
+
413
+ def success(oid, request, session)
414
+ session.clear
415
+ session[:openid_identity] = oid.display_identifier
416
+ session[:openid_identifier] = oid.identity_url
417
+ extensions.keys.each do |ext|
418
+ label = ext.name[/[^:]+$/].downcase
419
+ response = ext::Response.from_success_response(oid)
420
+ session[label] = response.data
426
421
  end
427
- @extensions[ext] = args
428
- return ext::NS_URI
422
+ redirect(realm)
429
423
  end
430
424
 
431
- # A conveniance method that returns the namespace of all current
432
- # extensions used by this instance.
433
- def extension_namespaces
434
- @extensions.keys.map{|e|e::NS_URI}
425
+ # Called if the Identity Provider indicates further setup by the user is
426
+ # required.
427
+ # The identifier is retrived from the openid session at :openid_param.
428
+ # And :setup_needed is set to true to prevent looping.
429
+
430
+ def setup_needed(oid, request, session)
431
+ identifier = session[:openid_param]
432
+ session[:setup_needed] = true
433
+ redirect req.script_name + '?' + openid_param + '=' + identifier
434
+ end
435
+
436
+ # Called if the user indicates they wish to cancel identification.
437
+ # Data within openid session is cleared.
438
+
439
+ def cancel(oid, request, session)
440
+ session.clear
441
+ access_denied
442
+ end
443
+
444
+ # Called if the Identity Provider indicates the user is unable to confirm
445
+ # their identity. Data within the openid session is left alone, in case
446
+ # of swarm auth attacks.
447
+
448
+ def failure(oid, request, session)
449
+ unauthorized
450
+ end
451
+ end
452
+
453
+ # A class developed out of the request to use OpenID as an authentication
454
+ # middleware. The request will be sent to the OpenID instance unless the
455
+ # block evaluates to true. For example in rackup, you can use it as such:
456
+ #
457
+ # use Rack::Session::Pool
458
+ # use Rack::Auth::OpenIDAuth, realm, openid_options do |env|
459
+ # env['rack.session'][:authkey] == a_string
460
+ # end
461
+ # run RackApp
462
+ #
463
+ # Or simply:
464
+ #
465
+ # app = Rack::Auth::OpenIDAuth.new app, realm, openid_options, &auth
466
+
467
+ class OpenIDAuth < Rack::Auth::AbstractHandler
468
+ attr_reader :oid
469
+ def initialize(app, realm, options={}, &auth)
470
+ @oid = OpenID.new(realm, options)
471
+ super(app, &auth)
472
+ end
473
+
474
+ def call(env)
475
+ to = auth.call(env) ? @app : @oid
476
+ to.call env
435
477
  end
436
478
  end
437
479
  end