rack 1.4.1 → 1.4.2

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 (81) hide show
  1. data/COPYING +1 -1
  2. data/KNOWN-ISSUES +9 -0
  3. data/README.rdoc +72 -7
  4. data/Rakefile +18 -11
  5. data/SPEC +3 -1
  6. data/contrib/rack.png +0 -0
  7. data/contrib/rack.svg +150 -0
  8. data/contrib/rdoc.css +412 -0
  9. data/lib/rack/auth/basic.rb +1 -1
  10. data/lib/rack/auth/digest/nonce.rb +1 -1
  11. data/lib/rack/backports/uri/common_18.rb +14 -28
  12. data/lib/rack/backports/uri/common_192.rb +14 -17
  13. data/lib/rack/backports/uri/common_193.rb +29 -0
  14. data/lib/rack/body_proxy.rb +10 -0
  15. data/lib/rack/builder.rb +1 -1
  16. data/lib/rack/cascade.rb +11 -0
  17. data/lib/rack/commonlogger.rb +18 -5
  18. data/lib/rack/deflater.rb +5 -1
  19. data/lib/rack/directory.rb +1 -1
  20. data/lib/rack/etag.rb +6 -3
  21. data/lib/rack/file.rb +13 -4
  22. data/lib/rack/head.rb +1 -0
  23. data/lib/rack/lint.rb +3 -1
  24. data/lib/rack/lock.rb +3 -4
  25. data/lib/rack/mime.rb +1 -1
  26. data/lib/rack/mock.rb +3 -2
  27. data/lib/rack/multipart.rb +2 -2
  28. data/lib/rack/multipart/parser.rb +6 -4
  29. data/lib/rack/reloader.rb +1 -1
  30. data/lib/rack/request.rb +2 -4
  31. data/lib/rack/response.rb +2 -1
  32. data/lib/rack/server.rb +28 -2
  33. data/lib/rack/session/abstract/id.rb +5 -0
  34. data/lib/rack/session/cookie.rb +9 -0
  35. data/lib/rack/static.rb +90 -8
  36. data/lib/rack/utils.rb +17 -10
  37. data/rack.gemspec +3 -3
  38. data/test/builder/line.ru +1 -0
  39. data/test/cgi/assets/folder/test.js +1 -0
  40. data/test/cgi/assets/fonts/font.eot +1 -0
  41. data/test/cgi/assets/images/image.png +1 -0
  42. data/test/cgi/assets/index.html +1 -0
  43. data/test/cgi/assets/javascripts/app.js +1 -0
  44. data/test/cgi/assets/stylesheets/app.css +1 -0
  45. data/test/spec_auth_basic.rb +8 -0
  46. data/test/spec_auth_digest.rb +14 -0
  47. data/test/spec_body_proxy.rb +4 -0
  48. data/test/spec_builder.rb +7 -1
  49. data/test/spec_cascade.rb +8 -0
  50. data/test/spec_chunked.rb +6 -6
  51. data/test/spec_config.rb +0 -1
  52. data/test/spec_content_length.rb +26 -13
  53. data/test/spec_content_type.rb +15 -5
  54. data/test/spec_deflater.rb +35 -17
  55. data/test/spec_directory.rb +20 -1
  56. data/test/spec_etag.rb +29 -13
  57. data/test/spec_file.rb +42 -25
  58. data/test/spec_head.rb +25 -7
  59. data/test/spec_lobster.rb +20 -5
  60. data/test/spec_lock.rb +46 -21
  61. data/test/spec_logger.rb +2 -7
  62. data/test/spec_methodoverride.rb +21 -22
  63. data/test/spec_mock.rb +12 -7
  64. data/test/spec_multipart.rb +29 -0
  65. data/test/spec_nulllogger.rb +13 -2
  66. data/test/spec_recursive.rb +12 -9
  67. data/test/spec_request.rb +2 -2
  68. data/test/spec_response.rb +30 -0
  69. data/test/spec_runtime.rb +15 -5
  70. data/test/spec_sendfile.rb +11 -8
  71. data/test/spec_server.rb +47 -0
  72. data/test/spec_session_cookie.rb +68 -1
  73. data/test/spec_session_memcache.rb +10 -8
  74. data/test/spec_session_pool.rb +13 -10
  75. data/test/spec_showexceptions.rb +9 -4
  76. data/test/spec_showstatus.rb +10 -5
  77. data/test/spec_static.rb +85 -9
  78. data/test/spec_urlmap.rb +10 -10
  79. data/test/spec_utils.rb +14 -1
  80. data/test/static/another/index.html +1 -0
  81. metadata +21 -8
@@ -101,7 +101,7 @@ module Rack
101
101
  return unless file
102
102
  stat = ::File.stat(file)
103
103
  return file, stat if stat.file?
104
- rescue Errno::ENOENT, Errno::ENOTDIR
104
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::ESRCH
105
105
  @cache.delete(file) and false
106
106
  end
107
107
  end
@@ -260,12 +260,10 @@ module Rack
260
260
  # the Cookie header such that those with more specific Path attributes
261
261
  # precede those with less specific. Ordering with respect to other
262
262
  # attributes (e.g., Domain) is unspecified.
263
- Utils.parse_query(string, ';,').each { |k,v| hash[k] = Array === v ? v.first : v }
263
+ cookies = Utils.parse_query(string, ';,') { |s| Rack::Utils.unescape(s) rescue s }
264
+ cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
264
265
  @env["rack.request.cookie_string"] = string
265
266
  hash
266
- rescue => error
267
- error.message.replace "cannot parse Cookie header: #{error.message}"
268
- raise
269
267
  end
270
268
 
271
269
  def xhr?
@@ -74,9 +74,10 @@ module Rack
74
74
  if [204, 205, 304].include?(status.to_i)
75
75
  header.delete "Content-Type"
76
76
  header.delete "Content-Length"
77
+ close
77
78
  [status.to_i, header, []]
78
79
  else
79
- [status.to_i, header, self]
80
+ [status.to_i, header, BodyProxy.new(self){}]
80
81
  end
81
82
  end
82
83
  alias to_a finish # For *response
@@ -26,7 +26,7 @@ module Rack
26
26
 
27
27
  opts.on("-I", "--include PATH",
28
28
  "specify $LOAD_PATH (may be used more than once)") { |path|
29
- options[:include] = path.split(":")
29
+ (options[:include] ||= []).concat(path.split(":"))
30
30
  }
31
31
 
32
32
  opts.on("-r", "--require LIBRARY",
@@ -247,11 +247,14 @@ module Rack
247
247
  pp app
248
248
  end
249
249
 
250
+ check_pid! if options[:pid]
251
+
250
252
  # Touch the wrapped app, so that the config.ru is loaded before
251
253
  # daemonization (i.e. before chdir, etc).
252
254
  wrapped_app
253
255
 
254
256
  daemonize_app if options[:daemonize]
257
+
255
258
  write_pid if options[:pid]
256
259
 
257
260
  trap(:INT) do
@@ -274,7 +277,7 @@ module Rack
274
277
  options = default_options
275
278
 
276
279
  # Don't evaluate CGI ISINDEX parameters.
277
- # http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
280
+ # http://www.meb.uni-bonn.de/docs/cgi/cl.html
278
281
  args.clear if ENV.include?("REQUEST_METHOD")
279
282
 
280
283
  options.merge! opt_parser.parse!(args)
@@ -319,5 +322,28 @@ module Rack
319
322
  ::File.open(options[:pid], 'w'){ |f| f.write("#{Process.pid}") }
320
323
  at_exit { ::File.delete(options[:pid]) if ::File.exist?(options[:pid]) }
321
324
  end
325
+
326
+ def check_pid!
327
+ case pidfile_process_status
328
+ when :running, :not_owned
329
+ $stderr.puts "A server is already running. Check #{options[:pid]}."
330
+ exit(1)
331
+ when :dead
332
+ ::File.delete(options[:pid])
333
+ end
334
+ end
335
+
336
+ def pidfile_process_status
337
+ return :exited unless ::File.exist?(options[:pid])
338
+
339
+ pid = ::File.read(options[:pid]).to_i
340
+ Process.kill(0, pid)
341
+ :running
342
+ rescue Errno::ESRCH
343
+ :dead
344
+ rescue Errno::EPERM
345
+ :not_owned
346
+ end
347
+
322
348
  end
323
349
  end
@@ -116,6 +116,11 @@ module Rack
116
116
  super
117
117
  end
118
118
 
119
+ def merge!(hash)
120
+ load_for_write!
121
+ super
122
+ end
123
+
119
124
  private
120
125
 
121
126
  def load_for_read!
@@ -82,6 +82,15 @@ module Rack
82
82
 
83
83
  def initialize(app, options={})
84
84
  @secrets = options.values_at(:secret, :old_secret).compact
85
+ warn <<-MSG unless @secrets.size >= 1
86
+ SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
87
+ This poses a security threat. It is strongly recommended that you
88
+ provide a secret to prevent exploits that may be possible from crafted
89
+ cookies. This will not be supported in future versions of Rack, and
90
+ future versions will even invalidate your existing user cookies.
91
+
92
+ Called from: #{caller[0]}.
93
+ MSG
85
94
  @coder = options[:coder] ||= Base64::Marshal.new
86
95
  super(app, options.merge!(:cookie_only => true))
87
96
  end
@@ -26,13 +26,58 @@ module Rack
26
26
  # directory but uses index.html as default route for "/"
27
27
  #
28
28
  # use Rack::Static, :urls => [""], :root => 'public', :index =>
29
- # 'public/index.html'
29
+ # 'index.html'
30
30
  #
31
- # Set a fixed Cache-Control header for all served files:
31
+ # Set custom HTTP Headers for based on rules:
32
32
  #
33
- # use Rack::Static, :root => 'public', :cache_control => 'public'
33
+ # use Rack::Static, :root => 'public',
34
+ # :header_rules => [
35
+ # [rule, {header_field => content, header_field => content}],
36
+ # [rule, {header_field => content}]
37
+ # ]
38
+ #
39
+ # Rules for selecting files:
40
+ #
41
+ # 1) All files
42
+ # Provide the :all symbol
43
+ # :all => Matches every file
44
+ #
45
+ # 2) Folders
46
+ # Provide the folder path as a string
47
+ # '/folder' or '/folder/subfolder' => Matches files in a certain folder
48
+ #
49
+ # 3) File Extensions
50
+ # Provide the file extensions as an array
51
+ # ['css', 'js'] or %w(css js) => Matches files ending in .css or .js
52
+ #
53
+ # 4) Regular Expressions / Regexp
54
+ # Provide a regular expression
55
+ # %r{\.(?:css|js)\z} => Matches files ending in .css or .js
56
+ # /\.(?:eot|ttf|otf|woff|svg)\z/ => Matches files ending in
57
+ # the most common web font formats (.eot, .ttf, .otf, .woff, .svg)
58
+ # Note: This Regexp is available as a shortcut, using the :fonts rule
59
+ #
60
+ # 5) Font Shortcut
61
+ # Provide the :fonts symbol
62
+ # :fonts => Uses the Regexp rule stated right above to match all common web font endings
63
+ #
64
+ # Rule Ordering:
65
+ # Rules are applied in the order that they are provided.
66
+ # List rather general rules above special ones.
67
+ #
68
+ # Complete example use case including HTTP header rules:
69
+ #
70
+ # use Rack::Static, :root => 'public',
71
+ # :header_rules => [
72
+ # # Cache all static files in public caches (e.g. Rack::Cache)
73
+ # # as well as in the browser
74
+ # [:all, {'Cache-Control' => 'public, max-age=31536000'}],
75
+ #
76
+ # # Provide web fonts with cross-origin access-control-headers
77
+ # # Firefox requires this when serving assets using a Content Delivery Network
78
+ # [:fonts, {'Access-Control-Allow-Origin' => '*'}]
79
+ # ]
34
80
  #
35
-
36
81
  class Static
37
82
 
38
83
  def initialize(app, options={})
@@ -40,12 +85,18 @@ module Rack
40
85
  @urls = options[:urls] || ["/favicon.ico"]
41
86
  @index = options[:index]
42
87
  root = options[:root] || Dir.pwd
43
- cache_control = options[:cache_control]
44
- @file_server = Rack::File.new(root, cache_control)
88
+
89
+ # HTTP Headers
90
+ @header_rules = options[:header_rules] || []
91
+ # Allow for legacy :cache_control option while prioritizing global header_rules setting
92
+ @header_rules.insert(0, [:all, {'Cache-Control' => options[:cache_control]}]) if options[:cache_control]
93
+ @headers = {}
94
+
95
+ @file_server = Rack::File.new(root, @headers)
45
96
  end
46
97
 
47
98
  def overwrite_file_path(path)
48
- @urls.kind_of?(Hash) && @urls.key?(path) || @index && path == '/'
99
+ @urls.kind_of?(Hash) && @urls.key?(path) || @index && path =~ /\/$/
49
100
  end
50
101
 
51
102
  def route_file(path)
@@ -60,12 +111,43 @@ module Rack
60
111
  path = env["PATH_INFO"]
61
112
 
62
113
  if can_serve(path)
63
- env["PATH_INFO"] = (path == '/' ? @index : @urls[path]) if overwrite_file_path(path)
114
+ env["PATH_INFO"] = (path =~ /\/$/ ? path + @index : @urls[path]) if overwrite_file_path(path)
115
+ @path = env["PATH_INFO"]
116
+ apply_header_rules
64
117
  @file_server.call(env)
65
118
  else
66
119
  @app.call(env)
67
120
  end
68
121
  end
69
122
 
123
+ # Convert HTTP header rules to HTTP headers
124
+ def apply_header_rules
125
+ @header_rules.each do |rule, headers|
126
+ apply_rule(rule, headers)
127
+ end
128
+ end
129
+
130
+ def apply_rule(rule, headers)
131
+ case rule
132
+ when :all # All files
133
+ set_headers(headers)
134
+ when :fonts # Fonts Shortcut
135
+ set_headers(headers) if @path.match(/\.(?:ttf|otf|eot|woff|svg)\z/)
136
+ when String # Folder
137
+ path = ::Rack::Utils.unescape(@path)
138
+ set_headers(headers) if (path.start_with?(rule) || path.start_with?('/' + rule))
139
+ when Array # Extension/Extensions
140
+ extensions = rule.join('|')
141
+ set_headers(headers) if @path.match(/\.(#{extensions})\z/)
142
+ when Regexp # Flexible Regexp
143
+ set_headers(headers) if @path.match(rule)
144
+ else
145
+ end
146
+ end
147
+
148
+ def set_headers(headers)
149
+ headers.each { |field, content| @headers[field] = content }
150
+ end
151
+
70
152
  end
71
153
  end
@@ -5,11 +5,14 @@ require 'tempfile'
5
5
  require 'rack/multipart'
6
6
 
7
7
  major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
8
+ ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
8
9
 
9
10
  if major == 1 && minor < 9
10
11
  require 'rack/backports/uri/common_18'
11
- elsif major == 1 && minor == 9 && patch < 3
12
+ elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 320 && RUBY_ENGINE != 'jruby'
12
13
  require 'rack/backports/uri/common_192'
14
+ elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
15
+ require 'rack/backports/uri/common_193'
13
16
  else
14
17
  require 'uri/common'
15
18
  end
@@ -60,11 +63,15 @@ module Rack
60
63
  # and ';' characters. You can also use this to parse
61
64
  # cookies by changing the characters used in the second
62
65
  # parameter (which defaults to '&;').
63
- def parse_query(qs, d = nil)
66
+ def parse_query(qs, d = nil, &unescaper)
67
+ unescaper ||= method(:unescape)
68
+
64
69
  params = KeySpaceConstrainedParams.new
65
70
 
66
71
  (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
67
- k, v = p.split('=', 2).map { |x| unescape(x) }
72
+ next if p.empty?
73
+ k, v = p.split('=', 2).map(&unescaper)
74
+ next unless k || v
68
75
 
69
76
  if cur = params[k]
70
77
  if cur.class == Array
@@ -309,16 +316,16 @@ module Rack
309
316
  def byte_ranges(env, size)
310
317
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
311
318
  http_range = env['HTTP_RANGE']
312
- return nil unless http_range
319
+ return nil unless http_range && http_range =~ /bytes=([^;]+)/
313
320
  ranges = []
314
- http_range.split(/,\s*/).each do |range_spec|
315
- matches = range_spec.match(/bytes=(\d*)-(\d*)/)
316
- return nil unless matches
317
- r0,r1 = matches[1], matches[2]
321
+ $1.split(/,\s*/).each do |range_spec|
322
+ return nil unless range_spec =~ /(\d*)-(\d*)/
323
+ r0,r1 = $1, $2
318
324
  if r0.empty?
319
325
  return nil if r1.empty?
320
326
  # suffix-byte-range-spec, represents trailing suffix of file
321
- r0 = [size - r1.to_i, 0].max
327
+ r0 = size - r1.to_i
328
+ r0 = 0 if r0 < 0
322
329
  r1 = size - 1
323
330
  else
324
331
  r0 = r0.to_i
@@ -442,7 +449,7 @@ module Rack
442
449
  end
443
450
 
444
451
  def []=(key, value)
445
- @size += key.size unless @params.key?(key)
452
+ @size += key.size if key && !@params.key?(key)
446
453
  raise RangeError, 'exceeded available parameter key space' if @size > @limit
447
454
  @params[key] = value
448
455
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "rack"
3
- s.version = "1.4.1"
3
+ s.version = "1.4.2"
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.summary = "a modular Ruby webserver interface"
6
6
 
@@ -11,7 +11,7 @@ the simplest way possible, it unifies and distills the API for web
11
11
  servers, web frameworks, and software in between (the so-called
12
12
  middleware) into a single method call.
13
13
 
14
- Also see http://rack.rubyforge.org.
14
+ Also see http://rack.github.com/.
15
15
  EOF
16
16
 
17
17
  s.files = Dir['{bin/*,contrib/*,example/*,lib/**/*,test/**/*}'] +
@@ -24,7 +24,7 @@ EOF
24
24
 
25
25
  s.author = 'Christian Neukirchen'
26
26
  s.email = 'chneukirchen@gmail.com'
27
- s.homepage = 'http://rack.rubyforge.org'
27
+ s.homepage = 'http://rack.github.com/'
28
28
  s.rubyforge_project = 'rack'
29
29
 
30
30
  s.add_development_dependency 'bacon'
@@ -0,0 +1 @@
1
+ run lambda{ |env| [200, {'Content-Type' => 'text/plain'}, [__LINE__.to_s]] }
@@ -0,0 +1 @@
1
+ ### TestFile ###
@@ -0,0 +1 @@
1
+ ### TestFile ###
@@ -0,0 +1 @@
1
+ ### TestFile ###
@@ -0,0 +1 @@
1
+ ### TestFile ###
@@ -0,0 +1 @@
1
+ ### TestFile ###
@@ -0,0 +1 @@
1
+ ### TestFile ###
@@ -66,6 +66,14 @@ describe Rack::Auth::Basic do
66
66
  end
67
67
  end
68
68
 
69
+ should 'return 400 Bad Request for a malformed authorization header' do
70
+ request 'HTTP_AUTHORIZATION' => '' do |response|
71
+ response.should.be.a.client_error
72
+ response.status.should.equal 400
73
+ response.should.not.include 'WWW-Authenticate'
74
+ end
75
+ end
76
+
69
77
  it 'takes realm as optional constructor arg' do
70
78
  app = Rack::Auth::Basic.new(unprotected_app, realm) { true }
71
79
  realm.should == app.realm
@@ -153,6 +153,20 @@ describe Rack::Auth::Digest::MD5 do
153
153
  end
154
154
  end
155
155
 
156
+ should 'not rechallenge if nonce is not stale' do
157
+ begin
158
+ Rack::Auth::Digest::Nonce.time_limit = 10
159
+
160
+ request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', :wait => 1 do |response|
161
+ response.status.should.equal 200
162
+ response.body.to_s.should.equal 'Hi Alice'
163
+ response.headers['WWW-Authenticate'].should.not =~ /\bstale=true\b/
164
+ end
165
+ ensure
166
+ Rack::Auth::Digest::Nonce.time_limit = nil
167
+ end
168
+ end
169
+
156
170
  should 'rechallenge with stale parameter if nonce is stale' do
157
171
  begin
158
172
  Rack::Auth::Digest::Nonce.time_limit = 1
@@ -62,4 +62,8 @@ describe Rack::BodyProxy do
62
62
  proxy.close
63
63
  closed.should.equal true
64
64
  end
65
+
66
+ should 'provide an #each method' do
67
+ Rack::BodyProxy.method_defined?(:each).should.equal true
68
+ end
65
69
  end
@@ -193,9 +193,15 @@ describe Rack::Builder do
193
193
 
194
194
  it "requires anything not ending in .ru" do
195
195
  $: << File.dirname(__FILE__)
196
- app, options = Rack::Builder.parse_file 'builder/anything'
196
+ app, * = Rack::Builder.parse_file 'builder/anything'
197
197
  Rack::MockRequest.new(app).get("/").body.to_s.should.equal 'OK'
198
198
  $:.pop
199
199
  end
200
+
201
+ it "sets __LINE__ correctly" do
202
+ app, options = Rack::Builder.parse_file config_file('line.ru')
203
+ options = nil # ignored, prevents warning
204
+ Rack::MockRequest.new(app).get("/").body.to_s.should.equal '1'
205
+ end
200
206
  end
201
207
  end
@@ -50,4 +50,12 @@ describe Rack::Cascade do
50
50
  cascade << app3
51
51
  Rack::MockRequest.new(cascade).get('/foo').should.be.ok
52
52
  end
53
+
54
+ should "close the body on cascade" do
55
+ body = StringIO.new
56
+ closer = lambda { |env| [404, {}, body] }
57
+ cascade = Rack::Cascade.new([closer, app3], [404])
58
+ Rack::MockRequest.new(cascade).get("/foo").should.be.ok
59
+ body.should.be.closed
60
+ end
53
61
  end