rack 1.2.8 → 1.3.0.beta

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 (89) hide show
  1. data/README +9 -177
  2. data/Rakefile +2 -1
  3. data/SPEC +2 -2
  4. data/lib/rack.rb +2 -13
  5. data/lib/rack/auth/abstract/request.rb +7 -5
  6. data/lib/rack/auth/digest/md5.rb +6 -2
  7. data/lib/rack/auth/digest/params.rb +5 -7
  8. data/lib/rack/auth/digest/request.rb +1 -1
  9. data/lib/rack/backports/uri/common.rb +64 -0
  10. data/lib/rack/builder.rb +60 -3
  11. data/lib/rack/chunked.rb +29 -22
  12. data/lib/rack/conditionalget.rb +35 -16
  13. data/lib/rack/content_length.rb +3 -3
  14. data/lib/rack/deflater.rb +5 -2
  15. data/lib/rack/etag.rb +38 -10
  16. data/lib/rack/file.rb +76 -43
  17. data/lib/rack/handler.rb +13 -7
  18. data/lib/rack/handler/cgi.rb +0 -2
  19. data/lib/rack/handler/fastcgi.rb +13 -4
  20. data/lib/rack/handler/lsws.rb +0 -2
  21. data/lib/rack/handler/mongrel.rb +12 -2
  22. data/lib/rack/handler/scgi.rb +9 -1
  23. data/lib/rack/handler/thin.rb +7 -1
  24. data/lib/rack/handler/webrick.rb +12 -5
  25. data/lib/rack/lint.rb +2 -2
  26. data/lib/rack/lock.rb +29 -3
  27. data/lib/rack/methodoverride.rb +1 -1
  28. data/lib/rack/mime.rb +2 -2
  29. data/lib/rack/mock.rb +28 -33
  30. data/lib/rack/multipart.rb +34 -0
  31. data/lib/rack/multipart/generator.rb +93 -0
  32. data/lib/rack/multipart/parser.rb +164 -0
  33. data/lib/rack/multipart/uploaded_file.rb +30 -0
  34. data/lib/rack/request.rb +55 -19
  35. data/lib/rack/response.rb +10 -8
  36. data/lib/rack/sendfile.rb +14 -18
  37. data/lib/rack/server.rb +55 -8
  38. data/lib/rack/session/abstract/id.rb +233 -22
  39. data/lib/rack/session/cookie.rb +99 -46
  40. data/lib/rack/session/memcache.rb +30 -56
  41. data/lib/rack/session/pool.rb +22 -43
  42. data/lib/rack/showexceptions.rb +40 -11
  43. data/lib/rack/showstatus.rb +9 -2
  44. data/lib/rack/static.rb +29 -9
  45. data/lib/rack/urlmap.rb +6 -1
  46. data/lib/rack/utils.rb +67 -326
  47. data/rack.gemspec +2 -3
  48. data/test/builder/anything.rb +5 -0
  49. data/test/builder/comment.ru +4 -0
  50. data/test/builder/end.ru +3 -0
  51. data/test/builder/options.ru +2 -0
  52. data/test/cgi/lighttpd.conf +1 -1
  53. data/test/cgi/lighttpd.errors +412 -0
  54. data/test/multipart/content_type_and_no_filename +6 -0
  55. data/test/multipart/text +5 -0
  56. data/test/multipart/webkit +32 -0
  57. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  58. data/test/spec_auth_digest.rb +20 -5
  59. data/test/spec_builder.rb +29 -0
  60. data/test/spec_cgi.rb +11 -0
  61. data/test/spec_chunked.rb +1 -1
  62. data/test/spec_commonlogger.rb +1 -1
  63. data/test/spec_conditionalget.rb +47 -0
  64. data/test/spec_content_length.rb +0 -6
  65. data/test/spec_content_type.rb +5 -5
  66. data/test/spec_deflater.rb +46 -2
  67. data/test/spec_etag.rb +68 -1
  68. data/test/spec_fastcgi.rb +11 -0
  69. data/test/spec_file.rb +54 -3
  70. data/test/spec_handler.rb +23 -5
  71. data/test/spec_lint.rb +2 -2
  72. data/test/spec_lock.rb +111 -5
  73. data/test/spec_methodoverride.rb +2 -2
  74. data/test/spec_mock.rb +3 -3
  75. data/test/spec_mongrel.rb +1 -2
  76. data/test/spec_multipart.rb +279 -0
  77. data/test/spec_request.rb +222 -38
  78. data/test/spec_response.rb +9 -3
  79. data/test/spec_server.rb +74 -0
  80. data/test/spec_session_abstract_id.rb +43 -0
  81. data/test/spec_session_cookie.rb +97 -15
  82. data/test/spec_session_memcache.rb +60 -50
  83. data/test/spec_session_pool.rb +63 -40
  84. data/test/spec_showexceptions.rb +64 -0
  85. data/test/spec_static.rb +23 -0
  86. data/test/spec_utils.rb +65 -351
  87. data/test/spec_webrick.rb +23 -4
  88. metadata +35 -15
  89. data/test/spec_auth.rb +0 -57
@@ -47,6 +47,12 @@ module Rack
47
47
  opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
48
48
  options[:Port] = port
49
49
  }
50
+
51
+ opts.on("-O", "--option NAME[=VALUE]", "pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$0} -s SERVER -h' to get a list of options for SERVER") { |name|
52
+ name, value = name.split('=', 2)
53
+ value = true if value.nil?
54
+ options[name.to_sym] = value
55
+ }
50
56
 
51
57
  opts.on("-E", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") { |e|
52
58
  options[:environment] = e
@@ -57,26 +63,57 @@ module Rack
57
63
  }
58
64
 
59
65
  opts.on("-P", "--pid FILE", "file to store PID (default: rack.pid)") { |f|
60
- options[:pid] = f
66
+ options[:pid] = ::File.expand_path(f)
61
67
  }
62
68
 
63
69
  opts.separator ""
64
70
  opts.separator "Common options:"
65
71
 
66
- opts.on_tail("-h", "--help", "Show this message") do
72
+ opts.on_tail("-h", "-?", "--help", "Show this message") do
67
73
  puts opts
74
+ puts handler_opts(options)
75
+
68
76
  exit
69
77
  end
70
78
 
71
79
  opts.on_tail("--version", "Show version") do
72
- puts "Rack #{Rack.version}"
80
+ puts "Rack #{Rack.version} (Release: #{Rack.release})"
73
81
  exit
74
82
  end
75
83
  end
76
- opt_parser.parse! args
84
+
85
+ begin
86
+ opt_parser.parse! args
87
+ rescue OptionParser::InvalidOption => e
88
+ warn e.message
89
+ abort opt_parser.to_s
90
+ end
91
+
77
92
  options[:config] = args.last if args.last
78
93
  options
79
94
  end
95
+
96
+ def handler_opts(options)
97
+ begin
98
+ info = []
99
+ server = Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
100
+ if server && server.respond_to?(:valid_options)
101
+ info << ""
102
+ info << "Server-specific options for #{server.name}:"
103
+
104
+ has_options = false
105
+ server.valid_options.each do |name, description|
106
+ next if name.to_s.match(/^(Host|Port)[^a-zA-Z]/) # ignore handler's host and port options, we do our own.
107
+ info << " -O %-21s %s" % [name, description]
108
+ has_options = true
109
+ end
110
+ return "" if !has_options
111
+ end
112
+ info.join("\n")
113
+ rescue NameError
114
+ return "Warning: Could not find handler specified (#{options[:server] || 'default'}) to determine handler-specific options"
115
+ end
116
+ end
80
117
  end
81
118
 
82
119
  # Start a new rack server (like running rackup). This will parse ARGV and
@@ -136,6 +173,7 @@ module Rack
136
173
  # require the given libraries
137
174
  def initialize(options = nil)
138
175
  @options = options
176
+ @app = options[:app] if options && options[:app]
139
177
  end
140
178
 
141
179
  def options
@@ -144,7 +182,7 @@ module Rack
144
182
 
145
183
  def default_options
146
184
  {
147
- :environment => "development",
185
+ :environment => ENV['RACK_ENV'] || "development",
148
186
  :pid => nil,
149
187
  :Port => 9292,
150
188
  :Host => "0.0.0.0",
@@ -165,10 +203,20 @@ module Rack
165
203
  end
166
204
  end
167
205
 
206
+ def self.logging_middleware
207
+ lambda { |server|
208
+ server.server.name =~ /CGI/ ? nil : [Rack::CommonLogger, $stderr]
209
+ }
210
+ end
211
+
168
212
  def self.middleware
169
213
  @middleware ||= begin
170
214
  m = Hash.new {|h,k| h[k] = []}
171
- m["deployment"].concat [lambda {|server| server.server.name =~ /CGI/ ? nil : [Rack::CommonLogger, $stderr] }]
215
+ m["deployment"].concat [
216
+ [Rack::ContentLength],
217
+ [Rack::Chunked],
218
+ logging_middleware
219
+ ]
172
220
  m["development"].concat m["deployment"] + [[Rack::ShowExceptions], [Rack::Lint]]
173
221
  m
174
222
  end
@@ -229,7 +277,7 @@ module Rack
229
277
  # http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
230
278
  args.clear if ENV.include?("REQUEST_METHOD")
231
279
 
232
- options.merge! opt_parser.parse! args
280
+ options.merge! opt_parser.parse!(args)
233
281
  options[:config] = ::File.expand_path(options[:config])
234
282
  ENV["RACK_ENV"] = options[:environment]
235
283
  options
@@ -259,7 +307,6 @@ module Rack
259
307
  Process.setsid
260
308
  exit if fork
261
309
  Dir.chdir "/"
262
- ::File.umask 0000
263
310
  STDIN.reopen "/dev/null"
264
311
  STDOUT.reopen "/dev/null", "a"
265
312
  STDERR.reopen "/dev/null", "a"
@@ -4,12 +4,135 @@
4
4
  require 'time'
5
5
  require 'rack/request'
6
6
  require 'rack/response'
7
+ begin
8
+ require 'securerandom'
9
+ rescue LoadError
10
+ # We just won't get securerandom
11
+ end
7
12
 
8
13
  module Rack
9
14
 
10
15
  module Session
11
16
 
12
17
  module Abstract
18
+ ENV_SESSION_KEY = 'rack.session'.freeze
19
+ ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
20
+
21
+ # Thin wrapper around Hash that allows us to lazily load session id into session_options.
22
+
23
+ class OptionsHash < Hash #:nodoc:
24
+ def initialize(by, env, default_options)
25
+ @by = by
26
+ @env = env
27
+ @session_id_loaded = false
28
+ merge!(default_options)
29
+ end
30
+
31
+ def [](key)
32
+ load_session_id! if key == :id && session_id_not_loaded?
33
+ super
34
+ end
35
+
36
+ private
37
+
38
+ def session_id_not_loaded?
39
+ !key?(:id) && !@session_id_loaded
40
+ end
41
+
42
+ def load_session_id!
43
+ self[:id] = @by.send(:extract_session_id, @env)
44
+ @session_id_loaded = true
45
+ end
46
+ end
47
+
48
+ # SessionHash is responsible to lazily load the session from store.
49
+
50
+ class SessionHash < Hash
51
+ def initialize(by, env)
52
+ super()
53
+ @by = by
54
+ @env = env
55
+ @loaded = false
56
+ end
57
+
58
+ def [](key)
59
+ load_for_read!
60
+ super(key.to_s)
61
+ end
62
+
63
+ def has_key?(key)
64
+ load_for_read!
65
+ super(key.to_s)
66
+ end
67
+ alias :key? :has_key?
68
+ alias :include? :has_key?
69
+
70
+ def []=(key, value)
71
+ load_for_write!
72
+ super(key.to_s, value)
73
+ end
74
+
75
+ def clear
76
+ load_for_write!
77
+ super
78
+ end
79
+
80
+ def to_hash
81
+ load_for_read!
82
+ h = {}.replace(self)
83
+ h.delete_if { |k,v| v.nil? }
84
+ h
85
+ end
86
+
87
+ def update(hash)
88
+ load_for_write!
89
+ super(stringify_keys(hash))
90
+ end
91
+
92
+ def delete(key)
93
+ load_for_write!
94
+ super(key.to_s)
95
+ end
96
+
97
+ def inspect
98
+ load_for_read!
99
+ super
100
+ end
101
+
102
+ def exists?
103
+ return @exists if instance_variable_defined?(:@exists)
104
+ @exists = @by.send(:session_exists?, @env)
105
+ end
106
+
107
+ def loaded?
108
+ @loaded
109
+ end
110
+
111
+ private
112
+
113
+ def load_for_read!
114
+ load! if !loaded? && exists?
115
+ end
116
+
117
+ def load_for_write!
118
+ load! unless loaded?
119
+ end
120
+
121
+ def load!
122
+ id, session = @by.send(:load_session, @env)
123
+ @env[ENV_SESSION_OPTIONS_KEY][:id] = id
124
+ replace(stringify_keys(session))
125
+ @loaded = true
126
+ end
127
+
128
+ def stringify_keys(other)
129
+ hash = {}
130
+ other.each do |key, value|
131
+ hash[key.to_s] = value
132
+ end
133
+ hash
134
+ end
135
+ end
13
136
 
14
137
  # ID sets up a basic framework for implementing an id based sessioning
15
138
  # service. Cookies sent to the client for maintaining sessions will only
@@ -34,9 +157,13 @@ module Rack
34
157
  # recommended to change its value.
35
158
  #
36
159
  # Is Rack::Utils::Context compatible.
160
+ #
161
+ # Not included by default; you must require 'rack/session/abstract/id'
162
+ # to use.
37
163
 
38
164
  class ID
39
165
  DEFAULT_OPTIONS = {
166
+ :key => 'rack.session',
40
167
  :path => '/',
41
168
  :domain => nil,
42
169
  :expire_after => nil,
@@ -44,14 +171,19 @@ module Rack
44
171
  :httponly => true,
45
172
  :defer => false,
46
173
  :renew => false,
47
- :sidbits => 128
174
+ :sidbits => 128,
175
+ :cookie_only => true,
176
+ :secure_random => begin ::SecureRandom rescue false end
48
177
  }
49
178
 
50
179
  attr_reader :key, :default_options
180
+
51
181
  def initialize(app, options={})
52
182
  @app = app
53
- @key = options[:key] || "rack.session"
54
183
  @default_options = self.class::DEFAULT_OPTIONS.merge(options)
184
+ @key = options[:key] || "rack.session"
185
+ @cookie_only = @default_options.delete(:cookie_only)
186
+ initialize_sid
55
187
  end
56
188
 
57
189
  def call(env)
@@ -59,40 +191,91 @@ module Rack
59
191
  end
60
192
 
61
193
  def context(env, app=@app)
62
- load_session(env)
194
+ prepare_session(env)
63
195
  status, headers, body = app.call(env)
64
196
  commit_session(env, status, headers, body)
65
197
  end
66
198
 
67
199
  private
68
200
 
201
+ def initialize_sid
202
+ sidbits = @default_options.delete(:sidbits)
203
+ @sid_secure = @default_options.delete(:secure_random)
204
+ @sid_template = "%0#{sidbits / 4}x"
205
+ @sid_rand_width = (2**sidbits - 1)
206
+ end
207
+
69
208
  # Generate a new session id using Ruby #rand. The size of the
70
209
  # session id is controlled by the :sidbits option.
71
210
  # Monkey patch this to use custom methods for session id generation.
72
211
 
73
212
  def generate_sid
74
- "%0#{@default_options[:sidbits] / 4}x" %
75
- rand(2**@default_options[:sidbits] - 1)
213
+ r = if @sid_secure
214
+ SecureRandom.random_number(@sid_rand_width)
215
+ else
216
+ Kernel.rand(@sid_rand_width)
217
+ end
218
+ @sid_template % r
219
+ end
220
+
221
+ # Sets the lazy session at 'rack.session' and places options and session
222
+ # metadata into 'rack.session.options'.
223
+
224
+ def prepare_session(env)
225
+ env[ENV_SESSION_KEY] = SessionHash.new(self, env)
226
+ env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
76
227
  end
77
228
 
78
229
  # Extracts the session id from provided cookies and passes it and the
79
- # environment to #get_session. It then sets the resulting session into
80
- # 'rack.session', and places options and session metadata into
81
- # 'rack.session.options'.
230
+ # environment to #get_session.
82
231
 
83
232
  def load_session(env)
233
+ sid = current_session_id(env)
234
+ sid, session = get_session(env, sid)
235
+ [sid, session || {}]
236
+ end
237
+
238
+ # Extract session id from request object.
239
+
240
+ def extract_session_id(env)
84
241
  request = Rack::Request.new(env)
85
- session_id = request.cookies[@key]
242
+ sid = request.cookies[@key]
243
+ sid ||= request.params[@key] unless @cookie_only
244
+ sid
245
+ end
86
246
 
87
- begin
88
- session_id, session = get_session(env, session_id)
89
- env['rack.session'] = session
90
- rescue
91
- env['rack.session'] = Hash.new
92
- end
247
+ # Returns the current session id from the OptionsHash.
248
+
249
+ def current_session_id(env)
250
+ env[ENV_SESSION_OPTIONS_KEY][:id]
251
+ end
252
+
253
+ # Check if the session exists or not.
254
+
255
+ def session_exists?(env)
256
+ value = current_session_id(env)
257
+ value && !value.empty?
258
+ end
259
+
260
+ # Session should be commited if it was loaded, any of specific options like :renew, :drop
261
+ # or :expire_after was given and the security permissions match.
262
+
263
+ def commit_session?(env, session, options)
264
+ (loaded_session?(session) || force_options?(options)) && secure_session?(env, options)
265
+ end
266
+
267
+ def loaded_session?(session)
268
+ !session.is_a?(SessionHash) || session.loaded?
269
+ end
93
270
 
94
- env['rack.session.options'] = @default_options.
95
- merge(:id => session_id)
271
+ def force_options?(options)
272
+ options.values_at(:renew, :drop, :defer, :expire_after).any?
273
+ end
274
+
275
+ def secure_session?(env, options)
276
+ return true unless options[:secure]
277
+ request = Rack::Request.new(env)
278
+ request.ssl?
96
279
  end
97
280
 
98
281
  # Acquires the session from the environment and the session id from
@@ -103,22 +286,42 @@ module Rack
103
286
  def commit_session(env, status, headers, body)
104
287
  session = env['rack.session']
105
288
  options = env['rack.session.options']
106
- session_id = options[:id]
107
289
 
108
- if not session_id = set_session(env, session_id, session, options)
290
+ if options[:drop] || options[:renew]
291
+ session_id = destroy_session(env, options[:id] || generate_sid, options)
292
+ return [status, headers, body] unless session_id
293
+ end
294
+
295
+ return [status, headers, body] unless commit_session?(env, session, options)
296
+
297
+ session.send(:load!) unless loaded_session?(session)
298
+ session = session.to_hash
299
+ session_id ||= options[:id] || generate_sid
300
+
301
+ if not data = set_session(env, session_id, session, options)
109
302
  env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
110
303
  elsif options[:defer] and not options[:renew]
111
304
  env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
112
305
  else
113
306
  cookie = Hash.new
114
- cookie[:value] = session_id
115
- cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
116
- Utils.set_cookie_header!(headers, @key, cookie.merge(options))
307
+ cookie[:value] = data
308
+ cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
309
+ set_cookie(env, headers, cookie.merge!(options))
117
310
  end
118
311
 
119
312
  [status, headers, body]
120
313
  end
121
314
 
315
+ # Sets the cookie back to the client with session id. We skip the cookie
316
+ # setting if the value didn't change (sid is the same) or expires was given.
317
+
318
+ def set_cookie(env, headers, cookie)
319
+ request = Rack::Request.new(env)
320
+ if request.cookies[@key] != cookie[:value] || cookie[:expires]
321
+ Utils.set_cookie_header!(headers, @key, cookie)
322
+ end
323
+ end
324
+
122
325
  # All thread safety and session retrival proceedures should occur here.
123
326
  # Should return [session_id, session].
124
327
  # If nil is provided as the session id, generation of a new valid id
@@ -131,9 +334,17 @@ module Rack
131
334
  # All thread safety and session storage proceedures should occur here.
132
335
  # Should return true or false dependant on whether or not the session
133
336
  # was saved or not.
337
+
134
338
  def set_session(env, sid, session, options)
135
339
  raise '#set_session not implemented.'
136
340
  end
341
+
342
+ # All thread safety and session destroy proceedures should occur here.
343
+ # Should return a new session id or nil if options[:drop]
344
+
345
+ def destroy_session(env, sid, options)
346
+ raise '#destroy_session not implemented'
347
+ end
137
348
  end
138
349
  end
139
350
  end