rack 1.5.5 → 1.6.0.beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/KNOWN-ISSUES +14 -0
  3. data/README.rdoc +10 -6
  4. data/Rakefile +3 -4
  5. data/SPEC +59 -23
  6. data/lib/rack.rb +2 -1
  7. data/lib/rack/auth/abstract/request.rb +1 -1
  8. data/lib/rack/auth/basic.rb +1 -1
  9. data/lib/rack/auth/digest/md5.rb +1 -1
  10. data/lib/rack/backports/uri/common_18.rb +1 -1
  11. data/lib/rack/builder.rb +19 -4
  12. data/lib/rack/cascade.rb +2 -2
  13. data/lib/rack/chunked.rb +12 -1
  14. data/lib/rack/commonlogger.rb +13 -5
  15. data/lib/rack/conditionalget.rb +14 -2
  16. data/lib/rack/content_length.rb +5 -1
  17. data/lib/rack/deflater.rb +52 -13
  18. data/lib/rack/directory.rb +8 -2
  19. data/lib/rack/etag.rb +14 -6
  20. data/lib/rack/file.rb +10 -14
  21. data/lib/rack/handler.rb +2 -0
  22. data/lib/rack/handler/fastcgi.rb +4 -1
  23. data/lib/rack/handler/mongrel.rb +8 -2
  24. data/lib/rack/handler/scgi.rb +4 -1
  25. data/lib/rack/handler/thin.rb +8 -2
  26. data/lib/rack/handler/webrick.rb +46 -6
  27. data/lib/rack/head.rb +7 -2
  28. data/lib/rack/lint.rb +73 -25
  29. data/lib/rack/lobster.rb +8 -3
  30. data/lib/rack/methodoverride.rb +14 -3
  31. data/lib/rack/mime.rb +1 -15
  32. data/lib/rack/mock.rb +15 -7
  33. data/lib/rack/multipart.rb +2 -2
  34. data/lib/rack/multipart/parser.rb +107 -53
  35. data/lib/rack/multipart/uploaded_file.rb +2 -2
  36. data/lib/rack/nulllogger.rb +21 -2
  37. data/lib/rack/request.rb +38 -24
  38. data/lib/rack/response.rb +5 -0
  39. data/lib/rack/sendfile.rb +10 -5
  40. data/lib/rack/server.rb +45 -17
  41. data/lib/rack/session/abstract/id.rb +7 -6
  42. data/lib/rack/session/cookie.rb +17 -7
  43. data/lib/rack/session/memcache.rb +4 -4
  44. data/lib/rack/session/pool.rb +3 -6
  45. data/lib/rack/showexceptions.rb +20 -11
  46. data/lib/rack/showstatus.rb +1 -1
  47. data/lib/rack/static.rb +27 -30
  48. data/lib/rack/tempfile_reaper.rb +22 -0
  49. data/lib/rack/urlmap.rb +17 -3
  50. data/lib/rack/utils.rb +78 -47
  51. data/lib/rack/utils/okjson.rb +90 -91
  52. data/rack.gemspec +3 -3
  53. data/test/multipart/filename_and_no_name +6 -0
  54. data/test/multipart/invalid_character +6 -0
  55. data/test/spec_builder.rb +13 -4
  56. data/test/spec_chunked.rb +16 -0
  57. data/test/spec_commonlogger.rb +36 -0
  58. data/test/spec_content_length.rb +3 -1
  59. data/test/spec_deflater.rb +283 -148
  60. data/test/spec_etag.rb +11 -2
  61. data/test/spec_file.rb +11 -3
  62. data/test/spec_head.rb +2 -0
  63. data/test/spec_lobster.rb +1 -1
  64. data/test/spec_mock.rb +8 -0
  65. data/test/spec_multipart.rb +111 -49
  66. data/test/spec_request.rb +109 -25
  67. data/test/spec_response.rb +30 -0
  68. data/test/spec_server.rb +20 -5
  69. data/test/spec_session_cookie.rb +45 -2
  70. data/test/spec_session_memcache.rb +1 -1
  71. data/test/spec_showexceptions.rb +29 -36
  72. data/test/spec_showstatus.rb +19 -0
  73. data/test/spec_tempfile_reaper.rb +63 -0
  74. data/test/spec_urlmap.rb +23 -0
  75. data/test/spec_utils.rb +60 -10
  76. data/test/spec_webrick.rb +41 -0
  77. metadata +12 -9
  78. data/test/cgi/lighttpd.errors +0 -1
  79. data/test/multipart/three_files_three_fields +0 -31
@@ -1,4 +1,5 @@
1
1
  require 'rack/utils'
2
+ require 'rack/body_proxy'
2
3
 
3
4
  module Rack
4
5
 
@@ -22,7 +23,10 @@ module Rack
22
23
  obody = body
23
24
  body, length = [], 0
24
25
  obody.each { |part| body << part; length += bytesize(part) }
25
- obody.close if obody.respond_to?(:close)
26
+
27
+ body = BodyProxy.new(body) do
28
+ obody.close if obody.respond_to?(:close)
29
+ end
26
30
 
27
31
  headers['Content-Length'] = length.to_s
28
32
  end
@@ -17,19 +17,26 @@ module Rack
17
17
  # directive of 'no-transform' is present, or when the response status
18
18
  # code is one that doesn't allow an entity body.
19
19
  class Deflater
20
- def initialize(app)
20
+ ##
21
+ # Creates Rack::Deflater middleware.
22
+ #
23
+ # [app] rack app instance
24
+ # [options] hash of deflater options, i.e.
25
+ # 'if' - a lambda enabling / disabling deflation based on returned boolean value
26
+ # e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.length > 512 }
27
+ # 'include' - a list of content types that should be compressed
28
+ def initialize(app, options = {})
21
29
  @app = app
30
+
31
+ @condition = options[:if]
32
+ @compressible_types = options[:include]
22
33
  end
23
34
 
24
35
  def call(env)
25
36
  status, headers, body = @app.call(env)
26
37
  headers = Utils::HeaderHash.new(headers)
27
38
 
28
- # Skip compressing empty entity body responses and responses with
29
- # no-transform set.
30
- if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
31
- headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
32
- (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
39
+ unless should_deflate?(env, status, headers, body)
33
40
  return [status, headers, body]
34
41
  end
35
42
 
@@ -58,9 +65,9 @@ module Rack
58
65
  when "identity"
59
66
  [status, headers, body]
60
67
  when nil
61
- body.close if body.respond_to?(:close)
62
68
  message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
63
- [406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]]
69
+ bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
70
+ [406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, bp]
64
71
  end
65
72
  end
66
73
 
@@ -68,6 +75,7 @@ module Rack
68
75
  def initialize(body, mtime)
69
76
  @body = body
70
77
  @mtime = mtime
78
+ @closed = false
71
79
  end
72
80
 
73
81
  def each(&block)
@@ -79,7 +87,6 @@ module Rack
79
87
  gzip.flush
80
88
  }
81
89
  ensure
82
- @body.close if @body.respond_to?(:close)
83
90
  gzip.close
84
91
  @writer = nil
85
92
  end
@@ -87,6 +94,12 @@ module Rack
87
94
  def write(data)
88
95
  @writer.call(data)
89
96
  end
97
+
98
+ def close
99
+ return if @closed
100
+ @closed = true
101
+ @body.close if @body.respond_to?(:close)
102
+ end
90
103
  end
91
104
 
92
105
  class DeflateStream
@@ -100,17 +113,43 @@ module Rack
100
113
 
101
114
  def initialize(body)
102
115
  @body = body
116
+ @closed = false
103
117
  end
104
118
 
105
119
  def each
106
- deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
107
- @body.each { |part| yield deflater.deflate(part, Zlib::SYNC_FLUSH) }
108
- yield deflater.finish
120
+ deflator = ::Zlib::Deflate.new(*DEFLATE_ARGS)
121
+ @body.each { |part| yield deflator.deflate(part, Zlib::SYNC_FLUSH) }
122
+ yield deflator.finish
109
123
  nil
110
124
  ensure
125
+ deflator.close
126
+ end
127
+
128
+ def close
129
+ return if @closed
130
+ @closed = true
111
131
  @body.close if @body.respond_to?(:close)
112
- deflater.close
113
132
  end
114
133
  end
134
+
135
+ private
136
+
137
+ def should_deflate?(env, status, headers, body)
138
+ # Skip compressing empty entity body responses and responses with
139
+ # no-transform set.
140
+ if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
141
+ headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
142
+ (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
143
+ return false
144
+ end
145
+
146
+ # Skip if @compressible_types are given and does not include request's content type
147
+ return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/]))
148
+
149
+ # Skip if @condition lambda is given and evaluates to false
150
+ return false if @condition && !@condition.call(env, status, headers, body)
151
+
152
+ true
153
+ end
115
154
  end
116
155
  end
@@ -135,8 +135,8 @@ table { width:100%%; }
135
135
  end
136
136
 
137
137
  def each
138
- show_path = @path.sub(/^#{@root}/,'')
139
- files = @files.map{|f| DIR_FILE % f }*"\n"
138
+ show_path = Rack::Utils.escape_html(@path.sub(/^#{@root}/,''))
139
+ files = @files.map{|f| DIR_FILE % DIR_FILE_escape(*f) }*"\n"
140
140
  page = DIR_PAGE % [ show_path, show_path , files ]
141
141
  page.each_line{|l| yield l }
142
142
  end
@@ -157,5 +157,11 @@ table { width:100%%; }
157
157
 
158
158
  int.to_s + 'B'
159
159
  end
160
+
161
+ private
162
+ # Assumes url is already escaped.
163
+ def DIR_FILE_escape url, *html
164
+ [url, *html.map { |e| Utils.escape_html(e) }]
165
+ end
160
166
  end
161
167
  end
@@ -23,8 +23,12 @@ module Rack
23
23
  status, headers, body = @app.call(env)
24
24
 
25
25
  if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
26
- digest, body = digest_body(body)
27
- headers['ETag'] = %("#{digest}") if digest
26
+ original_body = body
27
+ digest, new_body = digest_body(body)
28
+ body = Rack::BodyProxy.new(new_body) do
29
+ original_body.close if original_body.respond_to?(:close)
30
+ end
31
+ headers['ETag'] = %(W/"#{digest}") if digest
28
32
  end
29
33
 
30
34
  unless headers['Cache-Control']
@@ -55,10 +59,14 @@ module Rack
55
59
 
56
60
  def digest_body(body)
57
61
  parts = []
58
- body.each { |part| parts << part }
59
- string_body = parts.join
60
- digest = Digest::MD5.hexdigest(string_body) unless string_body.empty?
61
- [digest, parts]
62
+ digest = nil
63
+
64
+ body.each do |part|
65
+ parts << part
66
+ (digest ||= Digest::MD5.new) << part unless part.empty?
67
+ end
68
+
69
+ [digest && digest.hexdigest, parts]
62
70
  end
63
71
  end
64
72
  end
@@ -12,8 +12,8 @@ module Rack
12
12
  # like sendfile on the +path+.
13
13
 
14
14
  class File
15
- SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
16
- ALLOWED_VERBS = %w[GET HEAD]
15
+ ALLOWED_VERBS = %w[GET HEAD OPTIONS]
16
+ ALLOW_HEADER = ALLOWED_VERBS.join(', ')
17
17
 
18
18
  attr_accessor :root
19
19
  attr_accessor :path
@@ -35,20 +35,13 @@ module Rack
35
35
 
36
36
  def _call(env)
37
37
  unless ALLOWED_VERBS.include? env["REQUEST_METHOD"]
38
- return fail(405, "Method Not Allowed")
38
+ return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
39
39
  end
40
40
 
41
41
  path_info = Utils.unescape(env["PATH_INFO"])
42
- parts = path_info.split SEPS
42
+ clean_path_info = Utils.clean_path_info(path_info)
43
43
 
44
- clean = []
45
-
46
- parts.each do |part|
47
- next if part.empty? || part == '.'
48
- part == '..' ? clean.pop : clean << part
49
- end
50
-
51
- @path = F.join(@root, *clean)
44
+ @path = F.join(@root, clean_path_info)
52
45
 
53
46
  available = begin
54
47
  F.file?(@path) && F.readable?(@path)
@@ -64,6 +57,9 @@ module Rack
64
57
  end
65
58
 
66
59
  def serving(env)
60
+ if env["REQUEST_METHOD"] == "OPTIONS"
61
+ return [200, {'Allow' => ALLOW_HEADER, 'Content-Length' => '0'}, []]
62
+ end
67
63
  last_modified = F.mtime(@path).httpdate
68
64
  return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
69
65
 
@@ -121,7 +117,7 @@ module Rack
121
117
 
122
118
  private
123
119
 
124
- def fail(status, body)
120
+ def fail(status, body, headers = {})
125
121
  body += "\n"
126
122
  [
127
123
  status,
@@ -129,7 +125,7 @@ module Rack
129
125
  "Content-Type" => "text/plain",
130
126
  "Content-Length" => body.size.to_s,
131
127
  "X-Cascade" => "pass"
132
- },
128
+ }.merge!(headers),
133
129
  [body]
134
130
  ]
135
131
  end
@@ -53,6 +53,8 @@ module Rack
53
53
  Rack::Handler::FastCGI
54
54
  elsif ENV.include?("REQUEST_METHOD")
55
55
  Rack::Handler::CGI
56
+ elsif ENV.include?("RACK_HANDLER")
57
+ self.get(ENV["RACK_HANDLER"])
56
58
  else
57
59
  pick ['thin', 'puma', 'webrick']
58
60
  end
@@ -30,8 +30,11 @@ module Rack
30
30
  end
31
31
 
32
32
  def self.valid_options
33
+ environment = ENV['RACK_ENV'] || 'development'
34
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
35
+
33
36
  {
34
- "Host=HOST" => "Hostname to listen on (default: localhost)",
37
+ "Host=HOST" => "Hostname to listen on (default: #{default_host})",
35
38
  "Port=PORT" => "Port to listen on (default: 8080)",
36
39
  "File=PATH" => "Creates a Domain socket at PATH instead of a TCP socket. Ignores Host and Port if set.",
37
40
  }
@@ -7,8 +7,11 @@ module Rack
7
7
  module Handler
8
8
  class Mongrel < ::Mongrel::HttpHandler
9
9
  def self.run(app, options={})
10
+ environment = ENV['RACK_ENV'] || 'development'
11
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
12
+
10
13
  server = ::Mongrel::HttpServer.new(
11
- options[:Host] || '0.0.0.0',
14
+ options[:Host] || default_host,
12
15
  options[:Port] || 8080,
13
16
  options[:num_processors] || 950,
14
17
  options[:throttle] || 0,
@@ -39,8 +42,11 @@ module Rack
39
42
  end
40
43
 
41
44
  def self.valid_options
45
+ environment = ENV['RACK_ENV'] || 'development'
46
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
47
+
42
48
  {
43
- "Host=HOST" => "Hostname to listen on (default: localhost)",
49
+ "Host=HOST" => "Hostname to listen on (default: #{default_host})",
44
50
  "Port=PORT" => "Port to listen on (default: 8080)",
45
51
  "Processors=N" => "Number of concurrent processors to accept (default: 950)",
46
52
  "Timeout=N" => "Time before a request is dropped for inactivity (default: 60)",
@@ -17,8 +17,11 @@ module Rack
17
17
  end
18
18
 
19
19
  def self.valid_options
20
+ environment = ENV['RACK_ENV'] || 'development'
21
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
22
+
20
23
  {
21
- "Host=HOST" => "Hostname to listen on (default: localhost)",
24
+ "Host=HOST" => "Hostname to listen on (default: #{default_host})",
22
25
  "Port=PORT" => "Port to listen on (default: 8080)",
23
26
  }
24
27
  end
@@ -6,7 +6,10 @@ module Rack
6
6
  module Handler
7
7
  class Thin
8
8
  def self.run(app, options={})
9
- host = options.delete(:Host) || '0.0.0.0'
9
+ environment = ENV['RACK_ENV'] || 'development'
10
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
11
+
12
+ host = options.delete(:Host) || default_host
10
13
  port = options.delete(:Port) || 8080
11
14
  args = [host, port, app, options]
12
15
  # Thin versions below 0.8.0 do not support additional options
@@ -17,8 +20,11 @@ module Rack
17
20
  end
18
21
 
19
22
  def self.valid_options
23
+ environment = ENV['RACK_ENV'] || 'development'
24
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
25
+
20
26
  {
21
- "Host=HOST" => "Hostname to listen on (default: localhost)",
27
+ "Host=HOST" => "Hostname to listen on (default: #{default_host})",
22
28
  "Port=PORT" => "Port to listen on (default: 8080)",
23
29
  }
24
30
  end
@@ -2,12 +2,33 @@ require 'webrick'
2
2
  require 'stringio'
3
3
  require 'rack/content_length'
4
4
 
5
+ # This monkey patch allows for applications to perform their own chunking
6
+ # through WEBrick::HTTPResponse iff rack is set to true.
7
+ class WEBrick::HTTPResponse
8
+ attr_accessor :rack
9
+
10
+ alias _rack_setup_header setup_header
11
+ def setup_header
12
+ app_chunking = rack && @header['transfer-encoding'] == 'chunked'
13
+
14
+ @chunked = app_chunking if app_chunking
15
+
16
+ _rack_setup_header
17
+
18
+ @chunked = false if app_chunking
19
+ end
20
+ end
21
+
5
22
  module Rack
6
23
  module Handler
7
24
  class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
8
25
  def self.run(app, options={})
9
- options[:BindAddress] = options.delete(:Host) if options[:Host]
26
+ environment = ENV['RACK_ENV'] || 'development'
27
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
28
+
29
+ options[:BindAddress] = options.delete(:Host) || default_host
10
30
  options[:Port] ||= 8080
31
+ options[:OutputBufferSize] = 5
11
32
  @server = ::WEBrick::HTTPServer.new(options)
12
33
  @server.mount "/", Rack::Handler::WEBrick, app
13
34
  yield @server if block_given?
@@ -15,8 +36,11 @@ module Rack
15
36
  end
16
37
 
17
38
  def self.valid_options
39
+ environment = ENV['RACK_ENV'] || 'development'
40
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
41
+
18
42
  {
19
- "Host=HOST" => "Hostname to listen on (default: localhost)",
43
+ "Host=HOST" => "Hostname to listen on (default: #{default_host})",
20
44
  "Port=PORT" => "Port to listen on (default: 8080)",
21
45
  }
22
46
  end
@@ -32,6 +56,7 @@ module Rack
32
56
  end
33
57
 
34
58
  def service(req, res)
59
+ res.rack = true
35
60
  env = req.meta_vars
36
61
  env.delete_if { |k, v| v.nil? }
37
62
 
@@ -46,7 +71,11 @@ module Rack
46
71
  "rack.multiprocess" => false,
47
72
  "rack.run_once" => false,
48
73
 
49
- "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
74
+ "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http",
75
+
76
+ "rack.hijack?" => true,
77
+ "rack.hijack" => lambda { raise NotImplementedError, "only partial hijack is supported."},
78
+ "rack.hijack_io" => nil,
50
79
  })
51
80
 
52
81
  env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
@@ -61,6 +90,8 @@ module Rack
61
90
  begin
62
91
  res.status = status.to_i
63
92
  headers.each { |k, vs|
93
+ next if k.downcase == "rack.hijack"
94
+
64
95
  if k.downcase == "set-cookie"
65
96
  res.cookies.concat vs.split("\n")
66
97
  else
@@ -69,9 +100,18 @@ module Rack
69
100
  res[k] = vs.split("\n").join(", ")
70
101
  end
71
102
  }
72
- body.each { |part|
73
- res.body << part
74
- }
103
+
104
+ io_lambda = headers["rack.hijack"]
105
+ if io_lambda
106
+ rd, wr = IO.pipe
107
+ res.body = rd
108
+ res.chunked = true
109
+ io_lambda.call wr
110
+ else
111
+ body.each { |part|
112
+ res.body << part
113
+ }
114
+ end
75
115
  ensure
76
116
  body.close if body.respond_to? :close
77
117
  end
@@ -1,3 +1,5 @@
1
+ require 'rack/body_proxy'
2
+
1
3
  module Rack
2
4
 
3
5
  class Head
@@ -11,8 +13,11 @@ class Head
11
13
  status, headers, body = @app.call(env)
12
14
 
13
15
  if env["REQUEST_METHOD"] == "HEAD"
14
- body.close if body.respond_to? :close
15
- [status, headers, []]
16
+ [
17
+ status, headers, Rack::BodyProxy.new([]) do
18
+ body.close if body.respond_to? :close
19
+ end
20
+ ]
16
21
  else
17
22
  [status, headers, body]
18
23
  end