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
@@ -4,14 +4,17 @@ module Rack
4
4
  #
5
5
  # Example:
6
6
  #
7
- # app = Rack::Builder.new {
7
+ # require 'rack/lobster'
8
+ # app = Rack::Builder.new do
8
9
  # use Rack::CommonLogger
9
10
  # use Rack::ShowExceptions
10
11
  # map "/lobster" do
11
12
  # use Rack::Lint
12
13
  # run Rack::Lobster.new
13
14
  # end
14
- # }
15
+ # end
16
+ #
17
+ # run app
15
18
  #
16
19
  # Or
17
20
  #
@@ -20,6 +23,8 @@ module Rack
20
23
  # lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'OK'] }
21
24
  # end
22
25
  #
26
+ # run app
27
+ #
23
28
  # +use+ adds a middleware to the stack, +run+ dispatches to an application.
24
29
  # You can use +map+ to construct a Rack::URLMap in a convenient way.
25
30
 
@@ -32,7 +37,7 @@ module Rack
32
37
  options = opts.parse! $1.split(/\s+/)
33
38
  end
34
39
  cfgfile.sub!(/^__END__\n.*/, '')
35
- app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app",
40
+ app = eval "Rack::Builder.new {\n" + cfgfile + "\n}.to_app",
36
41
  TOPLEVEL_BINDING, config
37
42
  else
38
43
  require config
@@ -50,14 +55,66 @@ module Rack
50
55
  self.new(&block).to_app
51
56
  end
52
57
 
58
+ # Specifies a middleware to use in a stack.
59
+ #
60
+ # class Middleware
61
+ # def initialize(app)
62
+ # @app = app
63
+ # end
64
+ #
65
+ # def call(env)
66
+ # env["rack.some_header"] = "setting an example"
67
+ # @app.call(env)
68
+ # end
69
+ # end
70
+ #
71
+ # use Middleware
72
+ # run lambda { |env| [200, { "Content-Type => "text/plain" }, ["OK"]] }
73
+ #
74
+ # All requests through to this application will first be processed by the middleware class.
75
+ # The +call+ method in this example sets an additional environment key which then can be
76
+ # referenced in the application if required.
53
77
  def use(middleware, *args, &block)
54
78
  @ins << lambda { |app| middleware.new(app, *args, &block) }
55
79
  end
56
80
 
81
+ # Takes an argument that is an object that responds to #call and returns a Rack response.
82
+ # The simplest form of this is a lambda object:
83
+ #
84
+ # run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
85
+ #
86
+ # However this could also be a class:
87
+ #
88
+ # class Heartbeat
89
+ # def self.call(env)
90
+ # [200, { "Content-Type" => "text/plain" }, ["OK"]]
91
+ # end
92
+ # end
93
+ #
94
+ # run Heartbeat
57
95
  def run(app)
58
96
  @ins << app #lambda { |nothing| app }
59
97
  end
60
98
 
99
+ # Creates a route within the application.
100
+ #
101
+ # Rack::Builder.app do
102
+ # map '/' do
103
+ # run Heartbeat
104
+ # end
105
+ # end
106
+ #
107
+ # The +use+ method can also be used here to specify middleware to run under a specific path:
108
+ #
109
+ # Rack::Builder.app do
110
+ # map '/' do
111
+ # use Middleware
112
+ # run Heartbeat
113
+ # end
114
+ # end
115
+ #
116
+ # This example includes a piece of middleware which will run before requests hit +Heartbeat+.
117
+ #
61
118
  def map(path, &block)
62
119
  if @ins.last.kind_of? Hash
63
120
  @ins.last[path] = self.class.new(&block).to_app
@@ -7,6 +7,32 @@ module Rack
7
7
  class Chunked
8
8
  include Rack::Utils
9
9
 
10
+ # A body wrapper that emits chunked responses
11
+ class Body
12
+ TERM = "\r\n"
13
+ TAIL = "0#{TERM}#{TERM}"
14
+
15
+ include Rack::Utils
16
+
17
+ def initialize(body)
18
+ @body = body
19
+ end
20
+
21
+ def each
22
+ term = TERM
23
+ @body.each do |chunk|
24
+ size = bytesize(chunk)
25
+ next if size == 0
26
+ yield [size.to_s(16), term, chunk, term].join
27
+ end
28
+ yield TAIL
29
+ end
30
+
31
+ def close
32
+ @body.close if @body.respond_to?(:close)
33
+ end
34
+ end
35
+
10
36
  def initialize(app)
11
37
  @app = app
12
38
  end
@@ -21,29 +47,10 @@ module Rack
21
47
  headers['Transfer-Encoding']
22
48
  [status, headers, body]
23
49
  else
24
- dup.chunk(status, headers, body)
25
- end
26
- end
27
-
28
- def chunk(status, headers, body)
29
- @body = body
30
- headers.delete('Content-Length')
31
- headers['Transfer-Encoding'] = 'chunked'
32
- [status, headers, self]
33
- end
34
-
35
- def each
36
- term = "\r\n"
37
- @body.each do |chunk|
38
- size = bytesize(chunk)
39
- next if size == 0
40
- yield [size.to_s(16), term, chunk, term].join
50
+ headers.delete('Content-Length')
51
+ headers['Transfer-Encoding'] = 'chunked'
52
+ [status, headers, Body.new(body)]
41
53
  end
42
- yield ["0", term, "", term].join
43
- end
44
-
45
- def close
46
- @body.close if @body.respond_to?(:close)
47
54
  end
48
55
  end
49
56
  end
@@ -20,28 +20,47 @@ module Rack
20
20
  end
21
21
 
22
22
  def call(env)
23
- return @app.call(env) unless %w[GET HEAD].include?(env['REQUEST_METHOD'])
24
-
25
- status, headers, body = @app.call(env)
26
- headers = Utils::HeaderHash.new(headers)
27
- if etag_matches?(env, headers) || modified_since?(env, headers)
28
- status = 304
29
- headers.delete('Content-Type')
30
- headers.delete('Content-Length')
31
- body = []
23
+ case env['REQUEST_METHOD']
24
+ when "GET", "HEAD"
25
+ status, headers, body = @app.call(env)
26
+ headers = Utils::HeaderHash.new(headers)
27
+ if status == 200 && fresh?(env, headers)
28
+ status = 304
29
+ headers.delete('Content-Type')
30
+ headers.delete('Content-Length')
31
+ body = []
32
+ end
33
+ [status, headers, body]
34
+ else
35
+ @app.call(env)
32
36
  end
33
- [status, headers, body]
34
37
  end
35
38
 
36
39
  private
37
- def etag_matches?(env, headers)
38
- etag = headers['Etag'] and etag == env['HTTP_IF_NONE_MATCH']
40
+
41
+ def fresh?(env, headers)
42
+ modified_since = env['HTTP_IF_MODIFIED_SINCE']
43
+ none_match = env['HTTP_IF_NONE_MATCH']
44
+
45
+ return false unless modified_since || none_match
46
+
47
+ success = true
48
+ success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
49
+ success &&= etag_matches?(none_match, headers) if none_match
50
+ success
39
51
  end
40
52
 
41
- def modified_since?(env, headers)
42
- last_modified = headers['Last-Modified'] and
43
- last_modified == env['HTTP_IF_MODIFIED_SINCE']
53
+ def etag_matches?(none_match, headers)
54
+ etag = headers['ETag'] and etag == none_match
44
55
  end
45
- end
46
56
 
57
+ def modified_since?(modified_since, headers)
58
+ last_modified = to_rfc2822(headers['Last-Modified']) and
59
+ modified_since >= last_modified
60
+ end
61
+
62
+ def to_rfc2822(since)
63
+ Time.rfc2822(since) rescue nil
64
+ end
65
+ end
47
66
  end
@@ -16,10 +16,10 @@ module Rack
16
16
  if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
17
17
  !headers['Content-Length'] &&
18
18
  !headers['Transfer-Encoding'] &&
19
- (body.respond_to?(:to_ary) || body.respond_to?(:to_str))
19
+ body.respond_to?(:to_ary)
20
20
 
21
- body = [body] if body.respond_to?(:to_str) # rack 0.4 compat
22
- length = body.to_ary.inject(0) { |len, part| len + bytesize(part) }
21
+ length = 0
22
+ body.each { |part| length += bytesize(part) }
23
23
  headers['Content-Length'] = length.to_s
24
24
  end
25
25
 
@@ -60,7 +60,10 @@ module Rack
60
60
  @writer = block
61
61
  gzip =::Zlib::GzipWriter.new(self)
62
62
  gzip.mtime = @mtime
63
- @body.each { |part| gzip.write(part) }
63
+ @body.each { |part|
64
+ gzip.write(part)
65
+ gzip.flush
66
+ }
64
67
  @body.close if @body.respond_to?(:close)
65
68
  gzip.close
66
69
  @writer = nil
@@ -86,7 +89,7 @@ module Rack
86
89
 
87
90
  def each
88
91
  deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
89
- @body.each { |part| yield deflater.deflate(part) }
92
+ @body.each { |part| yield deflater.deflate(part, Zlib::SYNC_FLUSH) }
90
93
  @body.close if @body.respond_to?(:close)
91
94
  yield deflater.finish
92
95
  nil
@@ -1,32 +1,60 @@
1
1
  require 'digest/md5'
2
2
 
3
3
  module Rack
4
- # Automatically sets the ETag header on all String bodies
4
+ # Automatically sets the ETag header on all String bodies.
5
+ #
6
+ # The ETag header is skipped if ETag or Last-Modified headers are sent or if
7
+ # a sendfile body (body.responds_to :to_path) is given (since such cases
8
+ # should be handled by apache/nginx).
9
+ #
10
+ # On initialization, you can pass two parameters: a Cache-Control directive
11
+ # used when Etag is absent and a directive when it is present. The first
12
+ # defaults to nil, while the second defaults to "max-age=0, privaute, must-revalidate"
5
13
  class ETag
6
- def initialize(app)
14
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
15
+
16
+ def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
7
17
  @app = app
18
+ @cache_control = cache_control
19
+ @no_cache_control = no_cache_control
8
20
  end
9
21
 
10
22
  def call(env)
11
23
  status, headers, body = @app.call(env)
12
24
 
13
- if !headers.has_key?('ETag')
25
+ if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
14
26
  digest, body = digest_body(body)
15
- headers['ETag'] = %("#{digest}")
27
+ headers['ETag'] = %("#{digest}") if digest
28
+ end
29
+
30
+ unless headers['Cache-Control']
31
+ headers['Cache-Control'] = digest ? @cache_control : @no_cache_control
16
32
  end
17
33
 
18
34
  [status, headers, body]
19
35
  end
20
36
 
21
37
  private
38
+
39
+ def etag_status?(status)
40
+ status == 200 || status == 201
41
+ end
42
+
43
+ def etag_body?(body)
44
+ !body.respond_to?(:to_path)
45
+ end
46
+
47
+ def skip_caching?(headers)
48
+ headers['Cache-Control'] == 'no-cache' ||
49
+ headers.key?('ETag') || headers.key?('Last-Modified')
50
+ end
51
+
22
52
  def digest_body(body)
23
- digest = Digest::MD5.new
24
53
  parts = []
25
- body.each do |part|
26
- digest << part
27
- parts << part
28
- end
29
- [digest.hexdigest, parts]
54
+ body.each { |part| parts << part }
55
+ string_body = parts.join
56
+ digest = Digest::MD5.hexdigest(string_body) unless string_body.empty?
57
+ [digest, parts]
30
58
  end
31
59
  end
32
60
  end
@@ -12,13 +12,17 @@ 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
+
15
17
  attr_accessor :root
16
18
  attr_accessor :path
19
+ attr_accessor :cache_control
17
20
 
18
21
  alias :to_path :path
19
22
 
20
- def initialize(root)
23
+ def initialize(root, cache_control = nil)
21
24
  @root = root
25
+ @cache_control = cache_control
22
26
  end
23
27
 
24
28
  def call(env)
@@ -29,64 +33,93 @@ module Rack
29
33
 
30
34
  def _call(env)
31
35
  @path_info = Utils.unescape(env["PATH_INFO"])
32
- return forbidden if @path_info.include? ".."
36
+ parts = @path_info.split SEPS
33
37
 
34
- @path = F.join(@root, @path_info)
38
+ return fail(403, "Forbidden") if parts.include? ".."
35
39
 
36
- begin
37
- if F.file?(@path) && F.readable?(@path)
38
- serving
39
- else
40
- raise Errno::EPERM
41
- end
40
+ @path = F.join(@root, *parts)
41
+
42
+ available = begin
43
+ F.file?(@path) && F.readable?(@path)
42
44
  rescue SystemCallError
43
- not_found
45
+ false
44
46
  end
45
- end
46
47
 
47
- def forbidden
48
- body = "Forbidden\n"
49
- [403, {"Content-Type" => "text/plain",
50
- "Content-Length" => body.size.to_s,
51
- "X-Cascade" => "pass"},
52
- [body]]
48
+ if available
49
+ serving(env)
50
+ else
51
+ fail(404, "File not found: #{@path_info}")
52
+ end
53
53
  end
54
54
 
55
- # NOTE:
56
- # We check via File::size? whether this file provides size info
57
- # via stat (e.g. /proc files often don't), otherwise we have to
58
- # figure it out by reading the whole file into memory. And while
59
- # we're at it we also use this as body then.
55
+ def serving(env)
56
+ # NOTE:
57
+ # We check via File::size? whether this file provides size info
58
+ # via stat (e.g. /proc files often don't), otherwise we have to
59
+ # figure it out by reading the whole file into memory.
60
+ size = F.size?(@path) || Utils.bytesize(F.read(@path))
60
61
 
61
- def serving
62
- if size = F.size?(@path)
63
- body = self
62
+ response = [
63
+ 200,
64
+ {
65
+ "Last-Modified" => F.mtime(@path).httpdate,
66
+ "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain')
67
+ },
68
+ self
69
+ ]
70
+ response[1].merge! 'Cache-Control' => @cache_control if @cache_control
71
+
72
+ ranges = Rack::Utils.byte_ranges(env, size)
73
+ if ranges.nil? || ranges.length > 1
74
+ # No ranges, or multiple ranges (which we don't support):
75
+ # TODO: Support multiple byte-ranges
76
+ response[0] = 200
77
+ @range = 0..size-1
78
+ elsif ranges.empty?
79
+ # Unsatisfiable. Return error, and file size:
80
+ response = fail(416, "Byte range unsatisfiable")
81
+ response[1]["Content-Range"] = "bytes */#{size}"
82
+ return response
64
83
  else
65
- body = [F.read(@path)]
66
- size = Utils.bytesize(body.first)
84
+ # Partial content:
85
+ @range = ranges[0]
86
+ response[0] = 206
87
+ response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
88
+ size = @range.end - @range.begin + 1
67
89
  end
68
90
 
69
- [200, {
70
- "Last-Modified" => F.mtime(@path).httpdate,
71
- "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain'),
72
- "Content-Length" => size.to_s
73
- }, body]
74
- end
75
-
76
- def not_found
77
- body = "File not found: #{@path_info}\n"
78
- [404, {"Content-Type" => "text/plain",
79
- "Content-Length" => body.size.to_s,
80
- "X-Cascade" => "pass"},
81
- [body]]
91
+ response[1]["Content-Length"] = size.to_s
92
+ response
82
93
  end
83
94
 
84
95
  def each
85
- F.open(@path, "rb") { |file|
86
- while part = file.read(8192)
96
+ F.open(@path, "rb") do |file|
97
+ file.seek(@range.begin)
98
+ remaining_len = @range.end-@range.begin+1
99
+ while remaining_len > 0
100
+ part = file.read([8192, remaining_len].min)
101
+ break unless part
102
+ remaining_len -= part.length
103
+
87
104
  yield part
88
105
  end
89
- }
106
+ end
90
107
  end
108
+
109
+ private
110
+
111
+ def fail(status, body)
112
+ body += "\n"
113
+ [
114
+ status,
115
+ {
116
+ "Content-Type" => "text/plain",
117
+ "Content-Length" => body.size.to_s,
118
+ "X-Cascade" => "pass"
119
+ },
120
+ [body]
121
+ ]
122
+ end
123
+
91
124
  end
92
125
  end