rack 0.4.0 → 0.9.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 (59) hide show
  1. data/RDOX +61 -3
  2. data/README +52 -37
  3. data/Rakefile +9 -0
  4. data/SPEC +6 -3
  5. data/bin/rackup +0 -0
  6. data/lib/rack.rb +7 -2
  7. data/lib/rack/adapter/camping.rb +1 -1
  8. data/lib/rack/auth/openid.rb +4 -3
  9. data/lib/rack/builder.rb +12 -1
  10. data/lib/rack/conditionalget.rb +43 -0
  11. data/lib/rack/content_length.rb +25 -0
  12. data/lib/rack/deflater.rb +29 -5
  13. data/lib/rack/directory.rb +82 -91
  14. data/lib/rack/file.rb +45 -76
  15. data/lib/rack/handler.rb +4 -0
  16. data/lib/rack/handler/evented_mongrel.rb +1 -1
  17. data/lib/rack/handler/fastcgi.rb +2 -0
  18. data/lib/rack/handler/mongrel.rb +6 -2
  19. data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
  20. data/lib/rack/handler/thin.rb +15 -0
  21. data/lib/rack/handler/webrick.rb +8 -4
  22. data/lib/rack/head.rb +19 -0
  23. data/lib/rack/lint.rb +74 -10
  24. data/lib/rack/lobster.rb +13 -13
  25. data/lib/rack/methodoverride.rb +27 -0
  26. data/lib/rack/mime.rb +204 -0
  27. data/lib/rack/request.rb +10 -1
  28. data/lib/rack/response.rb +7 -2
  29. data/lib/rack/session/abstract/id.rb +14 -1
  30. data/lib/rack/session/cookie.rb +19 -1
  31. data/lib/rack/session/memcache.rb +1 -1
  32. data/lib/rack/session/pool.rb +1 -1
  33. data/lib/rack/showexceptions.rb +5 -1
  34. data/lib/rack/showstatus.rb +3 -2
  35. data/lib/rack/urlmap.rb +1 -1
  36. data/lib/rack/utils.rb +42 -13
  37. data/test/cgi/lighttpd.conf +1 -1
  38. data/test/cgi/test +0 -0
  39. data/test/cgi/test.fcgi +0 -0
  40. data/test/cgi/test.ru +0 -0
  41. data/test/spec_rack_builder.rb +34 -0
  42. data/test/spec_rack_conditionalget.rb +41 -0
  43. data/test/spec_rack_content_length.rb +43 -0
  44. data/test/spec_rack_deflater.rb +49 -14
  45. data/test/spec_rack_file.rb +7 -0
  46. data/test/spec_rack_handler.rb +3 -3
  47. data/test/spec_rack_head.rb +30 -0
  48. data/test/spec_rack_lint.rb +79 -2
  49. data/test/spec_rack_methodoverride.rb +60 -0
  50. data/test/spec_rack_mock.rb +1 -1
  51. data/test/spec_rack_mongrel.rb +20 -1
  52. data/test/spec_rack_request.rb +46 -1
  53. data/test/spec_rack_response.rb +10 -3
  54. data/test/spec_rack_session_cookie.rb +33 -0
  55. data/test/spec_rack_thin.rb +90 -0
  56. data/test/spec_rack_utils.rb +20 -18
  57. data/test/spec_rack_webrick.rb +17 -0
  58. data/test/testrequest.rb +12 -0
  59. metadata +91 -5
data/lib/rack/request.rb CHANGED
@@ -113,6 +113,7 @@ module Rack
113
113
  Utils::Multipart.parse_multipart(env)
114
114
  @env["rack.request.form_vars"] = @env["rack.input"].read
115
115
  @env["rack.request.form_hash"] = Utils.parse_query(@env["rack.request.form_vars"])
116
+ @env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)
116
117
  end
117
118
  @env["rack.request.form_hash"]
118
119
  else
@@ -122,7 +123,7 @@ module Rack
122
123
 
123
124
  # The union of GET and POST data.
124
125
  def params
125
- self.GET.update(self.POST)
126
+ self.put? ? self.GET : self.GET.update(self.POST)
126
127
  rescue EOFError => e
127
128
  self.GET
128
129
  end
@@ -205,5 +206,13 @@ module Rack
205
206
  end
206
207
  end
207
208
  end
209
+
210
+ def ip
211
+ if addr = @env['HTTP_X_FORWARDED_FOR']
212
+ addr.split(',').last.strip
213
+ else
214
+ @env['REMOTE_ADDR']
215
+ end
216
+ end
208
217
  end
209
218
  end
data/lib/rack/response.rb CHANGED
@@ -23,6 +23,7 @@ module Rack
23
23
 
24
24
  @writer = lambda { |x| @body << x }
25
25
  @block = nil
26
+ @length = 0
26
27
 
27
28
  @body = []
28
29
 
@@ -59,12 +60,13 @@ module Rack
59
60
  # N.B.: cgi.rb uses spaces...
60
61
  expires = "; expires=" + value[:expires].clone.gmtime.
61
62
  strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
63
+ secure = "; secure" if value[:secure]
62
64
  value = value[:value]
63
65
  end
64
66
  value = [value] unless Array === value
65
67
  cookie = Utils.escape(key) + "=" +
66
68
  value.map { |v| Utils.escape v }.join("&") +
67
- "#{domain}#{path}#{expires}"
69
+ "#{domain}#{path}#{expires}#{secure}"
68
70
 
69
71
  case self["Set-Cookie"]
70
72
  when Array
@@ -98,6 +100,7 @@ module Rack
98
100
  header.delete "Content-Type"
99
101
  [status.to_i, header.to_hash, []]
100
102
  else
103
+ header["Content-Length"] ||= @length.to_s
101
104
  [status.to_i, header.to_hash, self]
102
105
  end
103
106
  end
@@ -110,7 +113,9 @@ module Rack
110
113
  end
111
114
 
112
115
  def write(str)
113
- @writer.call str.to_s
116
+ s = str.to_s
117
+ @length += s.size
118
+ @writer.call s
114
119
  str
115
120
  end
116
121
 
@@ -26,7 +26,10 @@ module Rack
26
26
  :key => 'rack.session',
27
27
  :path => '/',
28
28
  :domain => nil,
29
- :expire_after => nil
29
+ :expire_after => nil,
30
+ :secure => false,
31
+ :httponly => true,
32
+ :sidbits => 128
30
33
  }
31
34
 
32
35
  def initialize(app, options={})
@@ -50,6 +53,14 @@ module Rack
50
53
 
51
54
  private
52
55
 
56
+ # Generate a new session id using Ruby #rand. The size of the
57
+ # session id is controlled by the :sidbits option.
58
+ # Monkey patch this to use custom methods for session id generation.
59
+ def generate_sid
60
+ "%0#{@default_options[:sidbits] / 4}x" %
61
+ rand(2**@default_options[:sidbits] - 1)
62
+ end
63
+
53
64
  # Extracts the session id from provided cookies and passes it and the
54
65
  # environment to #get_session. It then sets the resulting session into
55
66
  # 'rack.session', and places options and session metadata into
@@ -110,6 +121,8 @@ module Rack
110
121
  expiry = time + options[:expire_after]
111
122
  cookie<< "; expires=#{expiry.httpdate}"
112
123
  end
124
+ cookie<< "; Secure" if options[:secure]
125
+ cookie<< "; HttpOnly" if options[:httponly]
113
126
 
114
127
  case a = (h = response[1])['Set-Cookie']
115
128
  when Array then a << cookie
@@ -1,3 +1,5 @@
1
+ require 'openssl'
2
+
1
3
  module Rack
2
4
 
3
5
  module Session
@@ -5,13 +7,15 @@ module Rack
5
7
  # Rack::Session::Cookie provides simple cookie based session management.
6
8
  # The session is a Ruby Hash stored as base64 encoded marshalled data
7
9
  # set to :key (default: rack.session).
10
+ # When the secret key is set, cookie data is checked for data integrity.
8
11
  #
9
12
  # Example:
10
13
  #
11
14
  # use Rack::Session::Cookie, :key => 'rack.session',
12
15
  # :domain => 'foo.com',
13
16
  # :path => '/',
14
- # :expire_after => 2592000
17
+ # :expire_after => 2592000,
18
+ # :secret => 'change_me'
15
19
  #
16
20
  # All parameters are optional.
17
21
 
@@ -20,6 +24,7 @@ module Rack
20
24
  def initialize(app, options={})
21
25
  @app = app
22
26
  @key = options[:key] || "rack.session"
27
+ @secret = options[:secret]
23
28
  @default_options = {:domain => nil,
24
29
  :path => "/",
25
30
  :expire_after => nil}.merge(options)
@@ -37,6 +42,11 @@ module Rack
37
42
  request = Rack::Request.new(env)
38
43
  session_data = request.cookies[@key]
39
44
 
45
+ if @secret && session_data
46
+ session_data, digest = session_data.split("--")
47
+ session_data = nil unless digest == generate_hmac(session_data)
48
+ end
49
+
40
50
  begin
41
51
  session_data = session_data.unpack("m*").first
42
52
  session_data = Marshal.load(session_data)
@@ -52,6 +62,10 @@ module Rack
52
62
  session_data = Marshal.dump(env["rack.session"])
53
63
  session_data = [session_data].pack("m*")
54
64
 
65
+ if @secret
66
+ session_data = "#{session_data}--#{generate_hmac(session_data)}"
67
+ end
68
+
55
69
  if session_data.size > (4096 - @key.size)
56
70
  env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.")
57
71
  [status, headers, body]
@@ -66,6 +80,10 @@ module Rack
66
80
  end
67
81
  end
68
82
 
83
+ def generate_hmac(data)
84
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, @secret, data)
85
+ end
86
+
69
87
  end
70
88
  end
71
89
  end
@@ -45,7 +45,7 @@ module Rack
45
45
  @mutex.synchronize do
46
46
  begin
47
47
  raise RuntimeError, 'Unique id finding looping excessively' if (lc+=1) > 1000
48
- sid = "%08x" % rand(0xffffffff)
48
+ sid = generate_sid
49
49
  ret = @pool.add(sid, session)
50
50
  end until /^STORED/ =~ ret
51
51
  end
@@ -40,7 +40,7 @@ module Rack
40
40
  unless sess = @pool[sid] and ((expires = sess[:expire_at]).nil? or expires > Time.now)
41
41
  @pool.delete_if{|k,v| expiry = v[:expire_at] and expiry < Time.now }
42
42
  begin
43
- sid = "%08x" % rand(0xffffffff)
43
+ sid = generate_sid
44
44
  end while @pool.has_key?(sid)
45
45
  end
46
46
  @pool[sid] ||= {}
@@ -22,7 +22,11 @@ module Rack
22
22
  def call(env)
23
23
  @app.call(env)
24
24
  rescue StandardError, LoadError, SyntaxError => e
25
- [500, {"Content-Type" => "text/html"}, pretty(env, e)]
25
+ backtrace = pretty(env, e)
26
+ [500,
27
+ {"Content-Type" => "text/html",
28
+ "Content-Length" => backtrace.join.size.to_s},
29
+ backtrace]
26
30
  end
27
31
 
28
32
  def pretty(env, exception)
@@ -18,10 +18,11 @@ module Rack
18
18
 
19
19
  def call(env)
20
20
  status, headers, body = @app.call(env)
21
+ headers = Utils::HeaderHash.new(headers)
22
+ empty = headers['Content-Length'].to_i <= 0
21
23
 
22
24
  # client or server error, or explicit message
23
- if status.to_i >= 400 &&
24
- (body.empty? rescue false) || env["rack.showstatus.detail"]
25
+ if (status.to_i >= 400 && empty) || env["rack.showstatus.detail"]
25
26
  req = Rack::Request.new(env)
26
27
  message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
27
28
  detail = env["rack.showstatus.detail"] || message
data/lib/rack/urlmap.rb CHANGED
@@ -26,7 +26,7 @@ module Rack
26
26
  location = location.chomp('/')
27
27
 
28
28
  [host, location, app]
29
- }.sort_by { |(h, l, a)| -l.size } # Longest path first
29
+ }.sort_by { |(h, l, a)| [-l.size, h.to_s.size] } # Longest path first
30
30
  end
31
31
 
32
32
  def call(env)
data/lib/rack/utils.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'set'
1
2
  require 'tempfile'
2
3
 
3
4
  module Rack
@@ -31,10 +32,10 @@ module Rack
31
32
 
32
33
  def parse_query(qs, d = '&;')
33
34
  params = {}
34
-
35
+
35
36
  (qs || '').split(/[#{d}] */n).each do |p|
36
37
  k, v = unescape(p).split('=', 2)
37
-
38
+
38
39
  if cur = params[k]
39
40
  if cur.class == Array
40
41
  params[k] << v
@@ -45,11 +46,11 @@ module Rack
45
46
  params[k] = v
46
47
  end
47
48
  end
48
-
49
+
49
50
  return params
50
51
  end
51
52
  module_function :parse_query
52
-
53
+
53
54
  def build_query(params)
54
55
  params.map { |k, v|
55
56
  if v.class == Array
@@ -155,9 +156,11 @@ module Rack
155
156
  end
156
157
  end
157
158
 
158
- # A case-normalizing Hash, adjusting on [] and []=.
159
+ # A case-insensitive Hash that preserves the original case of a
160
+ # header when set.
159
161
  class HeaderHash < Hash
160
162
  def initialize(hash={})
163
+ @names = {}
161
164
  hash.each { |k, v| self[k] = v }
162
165
  end
163
166
 
@@ -166,15 +169,35 @@ module Rack
166
169
  end
167
170
 
168
171
  def [](k)
169
- super capitalize(k)
172
+ super @names[k.downcase]
170
173
  end
171
174
 
172
175
  def []=(k, v)
173
- super capitalize(k), v
176
+ delete k
177
+ @names[k.downcase] = k
178
+ super k, v
179
+ end
180
+
181
+ def delete(k)
182
+ super @names.delete(k.downcase)
174
183
  end
175
184
 
176
- def capitalize(k)
177
- k.to_s.downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }
185
+ def include?(k)
186
+ @names.has_key? k.downcase
187
+ end
188
+
189
+ alias_method :has_key?, :include?
190
+ alias_method :member?, :include?
191
+ alias_method :key?, :include?
192
+
193
+ def merge!(other)
194
+ other.each { |k, v| self[k] = v }
195
+ self
196
+ end
197
+
198
+ def merge(other)
199
+ hash = dup
200
+ hash.merge! other
178
201
  end
179
202
  end
180
203
 
@@ -192,10 +215,11 @@ module Rack
192
215
  206 => 'Partial Content',
193
216
  300 => 'Multiple Choices',
194
217
  301 => 'Moved Permanently',
195
- 302 => 'Moved Temporarily',
218
+ 302 => 'Found',
196
219
  303 => 'See Other',
197
220
  304 => 'Not Modified',
198
221
  305 => 'Use Proxy',
222
+ 307 => 'Temporary Redirect',
199
223
  400 => 'Bad Request',
200
224
  401 => 'Unauthorized',
201
225
  402 => 'Payment Required',
@@ -204,7 +228,7 @@ module Rack
204
228
  405 => 'Method Not Allowed',
205
229
  406 => 'Not Acceptable',
206
230
  407 => 'Proxy Authentication Required',
207
- 408 => 'Request Time-out',
231
+ 408 => 'Request Timeout',
208
232
  409 => 'Conflict',
209
233
  410 => 'Gone',
210
234
  411 => 'Length Required',
@@ -212,14 +236,19 @@ module Rack
212
236
  413 => 'Request Entity Too Large',
213
237
  414 => 'Request-URI Too Large',
214
238
  415 => 'Unsupported Media Type',
239
+ 416 => 'Requested Range Not Satisfiable',
240
+ 417 => 'Expectation Failed',
215
241
  500 => 'Internal Server Error',
216
242
  501 => 'Not Implemented',
217
243
  502 => 'Bad Gateway',
218
244
  503 => 'Service Unavailable',
219
- 504 => 'Gateway Time-out',
220
- 505 => 'HTTP Version not supported'
245
+ 504 => 'Gateway Timeout',
246
+ 505 => 'HTTP Version Not Supported'
221
247
  }
222
248
 
249
+ # Responses with HTTP status codes that should not have an entity body
250
+ STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
251
+
223
252
  # A multipart form data parser, adapted from IOWA.
224
253
  #
225
254
  # Usually, Rack::Request#POST takes care of calling this.
@@ -3,7 +3,7 @@ server.document-root = "."
3
3
  server.errorlog = "lighttpd.errors"
4
4
  server.port = 9203
5
5
 
6
- server.event-handler = "freebsd-kqueue"
6
+ server.event-handler = "select"
7
7
 
8
8
  cgi.assign = ("/test" => "",
9
9
  # ".ru" => ""
data/test/cgi/test CHANGED
File without changes
data/test/cgi/test.fcgi CHANGED
File without changes
data/test/cgi/test.ru CHANGED
File without changes
@@ -2,6 +2,8 @@ require 'test/spec'
2
2
 
3
3
  require 'rack/builder'
4
4
  require 'rack/mock'
5
+ require 'rack/showexceptions'
6
+ require 'rack/auth/basic'
5
7
 
6
8
  context "Rack::Builder" do
7
9
  specify "chains apps by default" do
@@ -47,4 +49,36 @@ context "Rack::Builder" do
47
49
  response.body.to_s.should.equal 'Hi Boss'
48
50
  end
49
51
 
52
+ specify "has explicit #to_app" do
53
+ app = Rack::Builder.app do
54
+ use Rack::ShowExceptions
55
+ run lambda { |env| raise "bzzzt" }
56
+ end
57
+
58
+ Rack::MockRequest.new(app).get("/").should.be.server_error
59
+ Rack::MockRequest.new(app).get("/").should.be.server_error
60
+ Rack::MockRequest.new(app).get("/").should.be.server_error
61
+ end
62
+
63
+ specify "apps are initialized once" do
64
+ app = Rack::Builder.new do
65
+ class AppClass
66
+ def initialize
67
+ @called = 0
68
+ end
69
+ def call(env)
70
+ raise "bzzzt" if @called > 0
71
+ @called += 1
72
+ [200, {'Content-Type' => 'text/plain'}, 'OK']
73
+ end
74
+ end
75
+
76
+ use Rack::ShowExceptions
77
+ run AppClass.new
78
+ end
79
+
80
+ Rack::MockRequest.new(app).get("/").status.should.equal 200
81
+ Rack::MockRequest.new(app).get("/").should.be.server_error
82
+ end
83
+
50
84
  end
@@ -0,0 +1,41 @@
1
+ require 'test/spec'
2
+ require 'time'
3
+
4
+ require 'rack/mock'
5
+ require 'rack/conditionalget'
6
+
7
+ context "Rack::ConditionalGet" do
8
+ specify "should set a 304 status and truncate body when If-Modified-Since hits" do
9
+ timestamp = Time.now.httpdate
10
+ app = Rack::ConditionalGet.new(lambda { |env|
11
+ [200, {'Last-Modified'=>timestamp}, 'TEST'] })
12
+
13
+ response = Rack::MockRequest.new(app).
14
+ get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp)
15
+
16
+ response.status.should.be == 304
17
+ response.body.should.be.empty
18
+ end
19
+
20
+ specify "should set a 304 status and truncate body when If-None-Match hits" do
21
+ app = Rack::ConditionalGet.new(lambda { |env|
22
+ [200, {'Etag'=>'1234'}, 'TEST'] })
23
+
24
+ response = Rack::MockRequest.new(app).
25
+ get("/", 'HTTP_IF_NONE_MATCH' => '1234')
26
+
27
+ response.status.should.be == 304
28
+ response.body.should.be.empty
29
+ end
30
+
31
+ specify "should not affect non-GET/HEAD requests" do
32
+ app = Rack::ConditionalGet.new(lambda { |env|
33
+ [200, {'Etag'=>'1234'}, 'TEST'] })
34
+
35
+ response = Rack::MockRequest.new(app).
36
+ post("/", 'HTTP_IF_NONE_MATCH' => '1234')
37
+
38
+ response.status.should.be == 200
39
+ response.body.should.be == 'TEST'
40
+ end
41
+ end