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
@@ -1,54 +1,67 @@
1
1
  # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
2
2
  # bugrep: Andreas Zehnder
3
3
 
4
- require 'rack/utils'
5
4
  require 'time'
5
+ require 'rack/request'
6
+ require 'rack/response'
6
7
 
7
8
  module Rack
9
+
8
10
  module Session
11
+
9
12
  module Abstract
13
+
10
14
  # ID sets up a basic framework for implementing an id based sessioning
11
15
  # service. Cookies sent to the client for maintaining sessions will only
12
- # contain an id reference. Only #get_session and #set_session should
13
- # need to be overwritten.
16
+ # contain an id reference. Only #get_session and #set_session are
17
+ # required to be overwritten.
14
18
  #
15
19
  # All parameters are optional.
16
20
  # * :key determines the name of the cookie, by default it is
17
21
  # 'rack.session'
18
- # * :domain and :path set the related cookie values, by default
19
- # domain is nil, and the path is '/'.
20
- # * :expire_after is the number of seconds in which the session
21
- # cookie will expire. By default it is set not to provide any
22
- # expiry time.
22
+ # * :path, :domain, :expire_after, :secure, and :httponly set the related
23
+ # cookie options as by Rack::Response#add_cookie
24
+ # * :defer will not set a cookie in the response.
25
+ # * :renew (implementation dependent) will prompt the generation of a new
26
+ # session id, and migration of data to be referenced at the new id. If
27
+ # :defer is set, it will be overridden and the cookie will be set.
28
+ # * :sidbits sets the number of bits in length that a generated session
29
+ # id will be.
30
+ #
31
+ # These options can be set on a per request basis, at the location of
32
+ # env['rack.session.options']. Additionally the id of the session can be
33
+ # found within the options hash at the key :id. It is highly not
34
+ # recommended to change its value.
35
+ #
36
+ # Is Rack::Utils::Context compatible.
37
+
23
38
  class ID
24
- attr_reader :key
25
39
  DEFAULT_OPTIONS = {
26
- :key => 'rack.session',
27
40
  :path => '/',
28
41
  :domain => nil,
29
42
  :expire_after => nil,
30
43
  :secure => false,
31
44
  :httponly => true,
45
+ :defer => false,
46
+ :renew => false,
32
47
  :sidbits => 128
33
48
  }
34
49
 
50
+ attr_reader :key, :default_options
35
51
  def initialize(app, options={})
52
+ @app = app
53
+ @key = options[:key] || "rack.session"
36
54
  @default_options = self.class::DEFAULT_OPTIONS.merge(options)
37
- @key = @default_options[:key]
38
- @default_context = context app
39
55
  end
40
56
 
41
57
  def call(env)
42
- @default_context.call(env)
58
+ context(env)
43
59
  end
44
60
 
45
- def context(app)
46
- Rack::Utils::Context.new self, app do |env|
47
- load_session env
48
- response = app.call(env)
49
- commit_session env, response
50
- response
51
- end
61
+ def context(env, app=@app)
62
+ load_session(env)
63
+ status, headers, body = app.call(env)
64
+ commit_session(env, status, headers, body)
52
65
  end
53
66
 
54
67
  private
@@ -56,6 +69,7 @@ module Rack
56
69
  # Generate a new session id using Ruby #rand. The size of the
57
70
  # session id is controlled by the :sidbits option.
58
71
  # Monkey patch this to use custom methods for session id generation.
72
+
59
73
  def generate_sid
60
74
  "%0#{@default_options[:sidbits] / 4}x" %
61
75
  rand(2**@default_options[:sidbits] - 1)
@@ -65,87 +79,62 @@ module Rack
65
79
  # environment to #get_session. It then sets the resulting session into
66
80
  # 'rack.session', and places options and session metadata into
67
81
  # 'rack.session.options'.
82
+
68
83
  def load_session(env)
69
- sid = (env['HTTP_COOKIE']||'')[/#{@key}=([^,;]+)/,1]
70
- sid, session = get_session(env, sid)
71
- unless session.is_a?(Hash)
72
- puts 'Session: '+sid.inspect+"\n"+session.inspect if $DEBUG
73
- raise TypeError, 'Session not a Hash'
84
+ request = Rack::Request.new(env)
85
+ session_id = request.cookies[@key]
86
+
87
+ begin
88
+ session_id, session = get_session(env, session_id)
89
+ env['rack.session'] = session
90
+ rescue
91
+ env['rack.session'] = Hash.new
74
92
  end
75
93
 
76
- options = @default_options.
77
- merge({ :id => sid, :by => self, :at => Time.now })
78
-
79
- env['rack.session'] = session
80
- env['rack.session.options'] = options
81
-
82
- return true
94
+ env['rack.session.options'] = @default_options.
95
+ merge(:id => session_id)
83
96
  end
84
97
 
85
98
  # Acquires the session from the environment and the session id from
86
- # the session options and passes them to #set_session. It then
87
- # proceeds to set a cookie up in the response with the session's id.
88
- def commit_session(env, response)
89
- unless response.is_a?(Array)
90
- puts 'Response: '+response.inspect if $DEBUG
91
- raise ArgumentError, 'Response is not an array.'
92
- end
99
+ # the session options and passes them to #set_session. If successful
100
+ # and the :defer option is not true, a cookie will be added to the
101
+ # response with the session's id.
93
102
 
103
+ def commit_session(env, status, headers, body)
104
+ session = env['rack.session']
94
105
  options = env['rack.session.options']
95
- unless options.is_a?(Hash)
96
- puts 'Options: '+options.inspect if $DEBUG
97
- raise TypeError, 'Options not a Hash'
106
+ session_id = options[:id]
107
+
108
+ if not session_id = set_session(env, session_id, session, options)
109
+ env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
110
+ [status, headers, body]
111
+ elsif options[:defer] and not options[:renew]
112
+ env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
113
+ [status, headers, body]
114
+ else
115
+ cookie = Hash.new
116
+ cookie[:value] = session_id
117
+ cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
118
+ response = Rack::Response.new(body, status, headers)
119
+ response.set_cookie(@key, cookie.merge(options))
120
+ response.to_a
98
121
  end
99
-
100
- sid, time, z = options.values_at(:id, :at, :by)
101
- unless self == z
102
- warn "#{self} not managing this session."
103
- return
104
- end
105
-
106
- unless env['rack.session'].is_a?(Hash)
107
- warn 'Session: '+sid.inspect+"\n"+session.inspect if $DEBUG
108
- raise TypeError, 'Session not a Hash'
109
- end
110
-
111
- unless set_session(env, sid)
112
- warn "Session not saved." if $DEBUG
113
- warn "#{env['rack.session'].inspect} has been lost."if $DEBUG
114
- return false
115
- end
116
-
117
- cookie = Utils.escape(@key)+'='+Utils.escape(sid)
118
- cookie<< "; domain=#{options[:domain]}" if options[:domain]
119
- cookie<< "; path=#{options[:path]}" if options[:path]
120
- if options[:expire_after]
121
- expiry = time + options[:expire_after]
122
- cookie<< "; expires=#{expiry.httpdate}"
123
- end
124
- cookie<< "; Secure" if options[:secure]
125
- cookie<< "; HttpOnly" if options[:httponly]
126
-
127
- case a = (h = response[1])['Set-Cookie']
128
- when Array then a << cookie
129
- when String then h['Set-Cookie'] = [a, cookie]
130
- when nil then h['Set-Cookie'] = cookie
131
- end
132
-
133
- return true
134
122
  end
135
123
 
136
- # Should return [session_id, session]. All thread safety and session
137
- # retrival proceedures should occur here.
124
+ # All thread safety and session retrival proceedures should occur here.
125
+ # Should return [session_id, session].
138
126
  # If nil is provided as the session id, generation of a new valid id
139
127
  # should occur within.
128
+
140
129
  def get_session(env, sid)
141
- raise '#get_session needs to be implemented.'
130
+ raise '#get_session not implemented.'
142
131
  end
143
132
 
144
133
  # All thread safety and session storage proceedures should occur here.
145
134
  # Should return true or false dependant on whether or not the session
146
135
  # was saved or not.
147
- def set_session(env, sid)
148
- raise '#set_session needs to be implemented.'
136
+ def set_session(env, sid, session, options)
137
+ raise '#set_session not implemented.'
149
138
  end
150
139
  end
151
140
  end
@@ -1,4 +1,6 @@
1
1
  require 'openssl'
2
+ require 'rack/request'
3
+ require 'rack/response'
2
4
 
3
5
  module Rack
4
6
 
@@ -21,76 +21,88 @@ module Rack
21
21
 
22
22
  class Memcache < Abstract::ID
23
23
  attr_reader :mutex, :pool
24
- DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge({
24
+ DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
25
25
  :namespace => 'rack:session',
26
26
  :memcache_server => 'localhost:11211'
27
- })
28
27
 
29
28
  def initialize(app, options={})
30
29
  super
31
- @pool = MemCache.new @default_options[:memcache_server], @default_options
32
- unless @pool.servers.any?{|s|s.alive?}
33
- raise "#{self} unable to find server during initialization."
34
- end
30
+
35
31
  @mutex = Mutex.new
32
+ @pool = MemCache.
33
+ new @default_options[:memcache_server], @default_options
34
+ raise 'No memcache servers' unless @pool.servers.any?{|s|s.alive?}
36
35
  end
37
36
 
38
- private
37
+ def generate_sid
38
+ loop do
39
+ sid = super
40
+ break sid unless @pool.get(sid, true)
41
+ end
42
+ end
39
43
 
40
44
  def get_session(env, sid)
41
- session = sid && @pool.get(sid)
42
- unless session and session.is_a?(Hash)
45
+ session = @pool.get(sid) if sid
46
+ @mutex.lock if env['rack.multithread']
47
+ unless sid and session
48
+ env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
43
49
  session = {}
44
- lc = 0
45
- @mutex.synchronize do
46
- begin
47
- raise RuntimeError, 'Unique id finding looping excessively' if (lc+=1) > 1000
48
- sid = generate_sid
49
- ret = @pool.add(sid, session)
50
- end until /^STORED/ =~ ret
51
- end
52
- end
53
- class << session
54
- @deleted = []
55
- def delete key
56
- (@deleted||=[]) << key
57
- super
58
- end
50
+ sid = generate_sid
51
+ ret = @pool.add sid, session
52
+ raise "Session collision on '#{sid.inspect}'" unless /^STORED/ =~ ret
59
53
  end
60
- [sid, session]
54
+ session.instance_variable_set('@old', {}.merge(session))
55
+ return [sid, session]
61
56
  rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
62
57
  warn "#{self} is unable to find server."
63
58
  warn $!.inspect
64
59
  return [ nil, {} ]
60
+ ensure
61
+ @mutex.unlock if env['rack.multithread']
65
62
  end
66
63
 
67
- def set_session(env, sid)
68
- session = env['rack.session']
69
- options = env['rack.session.options']
70
- expiry = options[:expire_after] || 0
71
- o, s = @mutex.synchronize do
72
- old_session = @pool.get(sid)
73
- unless old_session.is_a?(Hash)
74
- warn 'Session not properly initialized.' if $DEBUG
75
- old_session = {}
76
- @pool.add sid, old_session, expiry
77
- end
78
- session.instance_eval do
79
- @deleted.each{|k| old_session.delete(k) } if defined? @deleted
80
- end
81
- @pool.set sid, old_session.merge(session), expiry
82
- [old_session, session]
64
+ def set_session(env, session_id, new_session, options)
65
+ expiry = options[:expire_after]
66
+ expiry = expiry.nil? ? 0 : expiry + 1
67
+
68
+ @mutex.lock if env['rack.multithread']
69
+ session = @pool.get(session_id) || {}
70
+ if options[:renew] or options[:drop]
71
+ @pool.delete session_id
72
+ return false if options[:drop]
73
+ session_id = generate_sid
74
+ @pool.add session_id, 0 # so we don't worry about cache miss on #set
83
75
  end
84
- s.each do |k,v|
85
- next unless o.has_key?(k) and v != o[k]
86
- warn "session value assignment collision at #{k.inspect}:"+
87
- "\n\t#{o[k].inspect}\n\t#{v.inspect}"
88
- end if $DEBUG and env['rack.multithread']
89
- return true
76
+ old_session = new_session.instance_variable_get('@old') || {}
77
+ session = merge_sessions session_id, old_session, new_session, session
78
+ @pool.set session_id, session, expiry
79
+ return session_id
90
80
  rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
91
81
  warn "#{self} is unable to find server."
92
82
  warn $!.inspect
93
83
  return false
84
+ ensure
85
+ @mutex.unlock if env['rack.multithread']
86
+ end
87
+
88
+ private
89
+
90
+ def merge_sessions sid, old, new, cur=nil
91
+ cur ||= {}
92
+ unless Hash === old and Hash === new
93
+ warn 'Bad old or new sessions provided.'
94
+ return cur
95
+ end
96
+
97
+ delete = old.keys - new.keys
98
+ warn "//@#{sid}: delete #{delete*','}" if $VERBOSE and not delete.empty?
99
+ delete.each{|k| cur.delete k }
100
+
101
+ update = new.keys.select{|k| new[k] != old[k] }
102
+ warn "//@#{sid}: update #{update*','}" if $VERBOSE and not update.empty?
103
+ update.each{|k| cur[k] = new[k] }
104
+
105
+ cur
94
106
  end
95
107
  end
96
108
  end
@@ -13,19 +13,20 @@ module Rack
13
13
  # In the context of a multithreaded environment, sessions being
14
14
  # committed to the pool is done in a merging manner.
15
15
  #
16
+ # The :drop option is available in rack.session.options if you with to
17
+ # explicitly remove the session from the session cache.
18
+ #
16
19
  # Example:
17
20
  # myapp = MyRackApp.new
18
21
  # sessioned = Rack::Session::Pool.new(myapp,
19
- # :key => 'rack.session',
20
22
  # :domain => 'foo.com',
21
- # :path => '/',
22
23
  # :expire_after => 2592000
23
24
  # )
24
25
  # Rack::Handler::WEBrick.run sessioned
25
26
 
26
27
  class Pool < Abstract::ID
27
28
  attr_reader :mutex, :pool
28
- DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.dup
29
+ DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge :drop => false
29
30
 
30
31
  def initialize(app, options={})
31
32
  super
@@ -33,40 +34,66 @@ module Rack
33
34
  @mutex = Mutex.new
34
35
  end
35
36
 
36
- private
37
+ def generate_sid
38
+ loop do
39
+ sid = super
40
+ break sid unless @pool.key? sid
41
+ end
42
+ end
37
43
 
38
44
  def get_session(env, sid)
39
- session = @mutex.synchronize do
40
- unless sess = @pool[sid] and ((expires = sess[:expire_at]).nil? or expires > Time.now)
41
- @pool.delete_if{|k,v| expiry = v[:expire_at] and expiry < Time.now }
42
- begin
43
- sid = generate_sid
44
- end while @pool.has_key?(sid)
45
- end
46
- @pool[sid] ||= {}
45
+ session = @pool[sid] if sid
46
+ @mutex.lock if env['rack.multithread']
47
+ unless sid and session
48
+ env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
49
+ session = {}
50
+ sid = generate_sid
51
+ @pool.store sid, session
47
52
  end
48
- [sid, session]
53
+ session.instance_variable_set('@old', {}.merge(session))
54
+ return [sid, session]
55
+ ensure
56
+ @mutex.unlock if env['rack.multithread']
49
57
  end
50
58
 
51
- def set_session(env, sid)
52
- options = env['rack.session.options']
53
- expiry = options[:expire_after] && options[:at]+options[:expire_after]
54
- @mutex.synchronize do
55
- old_session = @pool[sid]
56
- old_session[:expire_at] = expiry if expiry
57
- session = old_session.merge(env['rack.session'])
58
- @pool[sid] = session
59
- session.each do |k,v|
60
- next unless old_session.has_key?(k) and v != old_session[k]
61
- warn "session value assignment collision at #{k}: #{old_session[k]} <- #{v}"
62
- end if $DEBUG and env['rack.multithread']
59
+ def set_session(env, session_id, new_session, options)
60
+ @mutex.lock if env['rack.multithread']
61
+ session = @pool[session_id]
62
+ if options[:renew] or options[:drop]
63
+ @pool.delete session_id
64
+ return false if options[:drop]
65
+ session_id = generate_sid
66
+ @pool.store session_id, 0
63
67
  end
64
- return true
68
+ old_session = new_session.instance_variable_get('@old') || {}
69
+ session = merge_sessions session_id, old_session, new_session, session
70
+ @pool.store session_id, session
71
+ return session_id
65
72
  rescue
66
- warn "#{self} is unable to find server."
67
- warn "#{env['rack.session'].inspect} has been lost."
73
+ warn "#{new_session.inspect} has been lost."
68
74
  warn $!.inspect
69
- return false
75
+ ensure
76
+ @mutex.unlock if env['rack.multithread']
77
+ end
78
+
79
+ private
80
+
81
+ def merge_sessions sid, old, new, cur=nil
82
+ cur ||= {}
83
+ unless Hash === old and Hash === new
84
+ warn 'Bad old or new sessions provided.'
85
+ return cur
86
+ end
87
+
88
+ delete = old.keys - new.keys
89
+ warn "//@#{sid}: dropping #{delete*','}" if $DEBUG and not delete.empty?
90
+ delete.each{|k| cur.delete k }
91
+
92
+ update = new.keys.select{|k| new[k] != old[k] }
93
+ warn "//@#{sid}: updating #{update*','}" if $DEBUG and not update.empty?
94
+ update.each{|k| cur[k] = new[k] }
95
+
96
+ cur
70
97
  end
71
98
  end
72
99
  end