rack 1.6.13 → 2.0.9

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 (141) hide show
  1. checksums.yaml +4 -4
  2. data/COPYING +1 -1
  3. data/HISTORY.md +138 -8
  4. data/README.rdoc +18 -28
  5. data/Rakefile +6 -14
  6. data/SPEC +3 -3
  7. data/contrib/rack_logo.svg +164 -111
  8. data/example/protectedlobster.rb +1 -1
  9. data/example/protectedlobster.ru +1 -1
  10. data/lib/rack/auth/abstract/request.rb +5 -1
  11. data/lib/rack/auth/digest/params.rb +2 -3
  12. data/lib/rack/auth/digest/request.rb +1 -1
  13. data/lib/rack/body_proxy.rb +14 -9
  14. data/lib/rack/builder.rb +3 -3
  15. data/lib/rack/chunked.rb +5 -5
  16. data/lib/rack/{commonlogger.rb → common_logger.rb} +3 -3
  17. data/lib/rack/content_length.rb +2 -2
  18. data/lib/rack/deflater.rb +4 -39
  19. data/lib/rack/directory.rb +66 -54
  20. data/lib/rack/etag.rb +5 -4
  21. data/lib/rack/events.rb +154 -0
  22. data/lib/rack/file.rb +64 -40
  23. data/lib/rack/handler/cgi.rb +15 -16
  24. data/lib/rack/handler/fastcgi.rb +13 -14
  25. data/lib/rack/handler/lsws.rb +11 -11
  26. data/lib/rack/handler/scgi.rb +15 -15
  27. data/lib/rack/handler/thin.rb +3 -0
  28. data/lib/rack/handler/webrick.rb +24 -26
  29. data/lib/rack/handler.rb +3 -25
  30. data/lib/rack/head.rb +15 -17
  31. data/lib/rack/lint.rb +40 -40
  32. data/lib/rack/lobster.rb +1 -1
  33. data/lib/rack/lock.rb +15 -10
  34. data/lib/rack/logger.rb +2 -2
  35. data/lib/rack/media_type.rb +38 -0
  36. data/lib/rack/{methodoverride.rb → method_override.rb} +6 -6
  37. data/lib/rack/mime.rb +18 -5
  38. data/lib/rack/mock.rb +36 -54
  39. data/lib/rack/multipart/generator.rb +5 -5
  40. data/lib/rack/multipart/parser.rb +270 -157
  41. data/lib/rack/multipart/uploaded_file.rb +1 -2
  42. data/lib/rack/multipart.rb +35 -6
  43. data/lib/rack/{nulllogger.rb → null_logger.rb} +1 -1
  44. data/lib/rack/query_parser.rb +192 -0
  45. data/lib/rack/recursive.rb +8 -8
  46. data/lib/rack/request.rb +394 -305
  47. data/lib/rack/response.rb +130 -57
  48. data/lib/rack/rewindable_input.rb +1 -12
  49. data/lib/rack/runtime.rb +10 -18
  50. data/lib/rack/sendfile.rb +5 -7
  51. data/lib/rack/server.rb +30 -23
  52. data/lib/rack/session/abstract/id.rb +110 -75
  53. data/lib/rack/session/cookie.rb +24 -17
  54. data/lib/rack/session/memcache.rb +9 -9
  55. data/lib/rack/session/pool.rb +8 -8
  56. data/lib/rack/show_exceptions.rb +386 -0
  57. data/lib/rack/{showstatus.rb → show_status.rb} +3 -3
  58. data/lib/rack/static.rb +30 -5
  59. data/lib/rack/tempfile_reaper.rb +2 -2
  60. data/lib/rack/urlmap.rb +15 -14
  61. data/lib/rack/utils.rb +138 -211
  62. data/lib/rack.rb +70 -21
  63. data/rack.gemspec +10 -9
  64. data/test/builder/an_underscore_app.rb +5 -0
  65. data/test/builder/options.ru +1 -1
  66. data/test/cgi/test.fcgi +1 -0
  67. data/test/cgi/test.gz +0 -0
  68. data/test/helper.rb +34 -0
  69. data/test/multipart/filename_with_encoded_words +7 -0
  70. data/test/multipart/filename_with_single_quote +7 -0
  71. data/test/multipart/quoted +15 -0
  72. data/test/multipart/rack-logo.png +0 -0
  73. data/test/multipart/unity3d_wwwform +11 -0
  74. data/test/registering_handler/rack/handler/registering_myself.rb +1 -1
  75. data/test/spec_auth_basic.rb +27 -19
  76. data/test/spec_auth_digest.rb +47 -46
  77. data/test/spec_body_proxy.rb +27 -27
  78. data/test/spec_builder.rb +51 -41
  79. data/test/spec_cascade.rb +24 -22
  80. data/test/spec_cgi.rb +49 -67
  81. data/test/spec_chunked.rb +37 -35
  82. data/test/{spec_commonlogger.rb → spec_common_logger.rb} +23 -21
  83. data/test/{spec_conditionalget.rb → spec_conditional_get.rb} +29 -28
  84. data/test/spec_config.rb +3 -2
  85. data/test/spec_content_length.rb +18 -17
  86. data/test/spec_content_type.rb +13 -12
  87. data/test/spec_deflater.rb +85 -49
  88. data/test/spec_directory.rb +87 -27
  89. data/test/spec_etag.rb +32 -31
  90. data/test/spec_events.rb +133 -0
  91. data/test/spec_fastcgi.rb +50 -72
  92. data/test/spec_file.rb +120 -77
  93. data/test/spec_handler.rb +19 -34
  94. data/test/spec_head.rb +15 -14
  95. data/test/spec_lint.rb +164 -199
  96. data/test/spec_lobster.rb +24 -23
  97. data/test/spec_lock.rb +79 -39
  98. data/test/spec_logger.rb +4 -3
  99. data/test/spec_media_type.rb +42 -0
  100. data/test/{spec_methodoverride.rb → spec_method_override.rb} +34 -35
  101. data/test/spec_mime.rb +19 -19
  102. data/test/spec_mock.rb +206 -144
  103. data/test/spec_multipart.rb +322 -200
  104. data/test/{spec_nulllogger.rb → spec_null_logger.rb} +5 -4
  105. data/test/spec_recursive.rb +17 -14
  106. data/test/spec_request.rb +780 -605
  107. data/test/spec_response.rb +233 -112
  108. data/test/spec_rewindable_input.rb +50 -40
  109. data/test/spec_runtime.rb +11 -10
  110. data/test/spec_sendfile.rb +30 -35
  111. data/test/spec_server.rb +78 -52
  112. data/test/spec_session_abstract_id.rb +11 -33
  113. data/test/spec_session_abstract_session_hash.rb +45 -0
  114. data/test/spec_session_cookie.rb +99 -67
  115. data/test/spec_session_memcache.rb +67 -68
  116. data/test/spec_session_pool.rb +52 -51
  117. data/test/{spec_showexceptions.rb → spec_show_exceptions.rb} +23 -28
  118. data/test/{spec_showstatus.rb → spec_show_status.rb} +36 -35
  119. data/test/spec_static.rb +71 -32
  120. data/test/spec_tempfile_reaper.rb +11 -10
  121. data/test/spec_thin.rb +55 -50
  122. data/test/spec_urlmap.rb +79 -78
  123. data/test/spec_utils.rb +441 -346
  124. data/test/spec_version.rb +2 -8
  125. data/test/spec_webrick.rb +93 -71
  126. data/test/static/foo.html +1 -0
  127. data/test/testrequest.rb +1 -1
  128. data/test/unregistered_handler/rack/handler/unregistered.rb +1 -1
  129. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +1 -1
  130. metadata +57 -36
  131. data/KNOWN-ISSUES +0 -44
  132. data/lib/rack/backports/uri/common_18.rb +0 -56
  133. data/lib/rack/backports/uri/common_192.rb +0 -52
  134. data/lib/rack/backports/uri/common_193.rb +0 -29
  135. data/lib/rack/handler/evented_mongrel.rb +0 -8
  136. data/lib/rack/handler/mongrel.rb +0 -106
  137. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  138. data/lib/rack/showexceptions.rb +0 -387
  139. data/lib/rack/utils/okjson.rb +0 -600
  140. data/test/spec_mongrel.rb +0 -182
  141. /data/lib/rack/{conditionalget.rb → conditional_get.rb} +0 -0
data/lib/rack/deflater.rb CHANGED
@@ -8,7 +8,6 @@ module Rack
8
8
  # Currently supported compression algorithms:
9
9
  #
10
10
  # * gzip
11
- # * deflate
12
11
  # * identity (no transformation)
13
12
  #
14
13
  # The middleware automatically detects when compression is supported
@@ -22,7 +21,7 @@ module Rack
22
21
  # [app] rack app instance
23
22
  # [options] hash of deflater options, i.e.
24
23
  # 'if' - a lambda enabling / disabling deflation based on returned boolean value
25
- # e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.length > 512 }
24
+ # e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.map(&:bytesize).reduce(0, :+) > 512 }
26
25
  # 'include' - a list of content types that should be compressed
27
26
  def initialize(app, options = {})
28
27
  @app = app
@@ -41,11 +40,11 @@ module Rack
41
40
 
42
41
  request = Request.new(env)
43
42
 
44
- encoding = Utils.select_best_encoding(%w(gzip deflate identity),
43
+ encoding = Utils.select_best_encoding(%w(gzip identity),
45
44
  request.accept_encoding)
46
45
 
47
46
  # Set the Vary HTTP header.
48
- vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
47
+ vary = headers["Vary"].to_s.split(",").map(&:strip)
49
48
  unless vary.include?("*") || vary.include?("Accept-Encoding")
50
49
  headers["Vary"] = vary.push("Accept-Encoding").join(",")
51
50
  end
@@ -57,10 +56,6 @@ module Rack
57
56
  mtime = headers.key?("Last-Modified") ?
58
57
  Time.httpdate(headers["Last-Modified"]) : Time.now
59
58
  [status, headers, GzipStream.new(body, mtime)]
60
- when "deflate"
61
- headers['Content-Encoding'] = "deflate"
62
- headers.delete(CONTENT_LENGTH)
63
- [status, headers, DeflateStream.new(body)]
64
59
  when "identity"
65
60
  [status, headers, body]
66
61
  when nil
@@ -101,36 +96,6 @@ module Rack
101
96
  end
102
97
  end
103
98
 
104
- class DeflateStream
105
- DEFLATE_ARGS = [
106
- Zlib::DEFAULT_COMPRESSION,
107
- # drop the zlib header which causes both Safari and IE to choke
108
- -Zlib::MAX_WBITS,
109
- Zlib::DEF_MEM_LEVEL,
110
- Zlib::DEFAULT_STRATEGY
111
- ]
112
-
113
- def initialize(body)
114
- @body = body
115
- @closed = false
116
- end
117
-
118
- def each
119
- deflator = ::Zlib::Deflate.new(*DEFLATE_ARGS)
120
- @body.each { |part| yield deflator.deflate(part, Zlib::SYNC_FLUSH) }
121
- yield deflator.finish
122
- nil
123
- ensure
124
- deflator.close
125
- end
126
-
127
- def close
128
- return if @closed
129
- @closed = true
130
- @body.close if @body.respond_to?(:close)
131
- end
132
- end
133
-
134
99
  private
135
100
 
136
101
  def should_deflate?(env, status, headers, body)
@@ -143,7 +108,7 @@ module Rack
143
108
  end
144
109
 
145
110
  # Skip if @compressible_types are given and does not include request's content type
146
- return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/]))
111
+ return false if @compressible_types && !(headers.has_key?(CONTENT_TYPE) && @compressible_types.include?(headers[CONTENT_TYPE][/[^;]*/]))
147
112
 
148
113
  # Skip if @condition lambda is given and evaluates to false
149
114
  return false if @condition && !@condition.call(env, status, headers, body)
@@ -39,58 +39,83 @@ table { width:100%%; }
39
39
  </body></html>
40
40
  PAGE
41
41
 
42
- attr_reader :files
43
- attr_accessor :root, :path
42
+ class DirectoryBody < Struct.new(:root, :path, :files)
43
+ def each
44
+ show_path = Rack::Utils.escape_html(path.sub(/^#{root}/,''))
45
+ listings = files.map{|f| DIR_FILE % DIR_FILE_escape(*f) }*"\n"
46
+ page = DIR_PAGE % [ show_path, show_path , listings ]
47
+ page.each_line{|l| yield l }
48
+ end
49
+
50
+ private
51
+ # Assumes url is already escaped.
52
+ def DIR_FILE_escape url, *html
53
+ [url, *html.map { |e| Utils.escape_html(e) }]
54
+ end
55
+ end
56
+
57
+ attr_reader :root, :path
44
58
 
45
59
  def initialize(root, app=nil)
46
- @root = F.expand_path(root)
60
+ @root = ::File.expand_path(root)
47
61
  @app = app || Rack::File.new(@root)
62
+ @head = Rack::Head.new(lambda { |env| get env })
48
63
  end
49
64
 
50
65
  def call(env)
51
- dup._call(env)
66
+ # strip body if this is a HEAD call
67
+ @head.call env
52
68
  end
53
69
 
54
- F = ::File
55
-
56
- def _call(env)
57
- @env = env
58
- @script_name = env[SCRIPT_NAME]
59
- @path_info = Utils.unescape(env[PATH_INFO])
70
+ def get(env)
71
+ script_name = env[SCRIPT_NAME]
72
+ path_info = Utils.unescape_path(env[PATH_INFO])
60
73
 
61
- if forbidden = check_forbidden
74
+ if bad_request = check_bad_request(path_info)
75
+ bad_request
76
+ elsif forbidden = check_forbidden(path_info)
62
77
  forbidden
63
78
  else
64
- @path = F.join(@root, @path_info)
65
- list_path
79
+ path = ::File.join(@root, path_info)
80
+ list_path(env, path, path_info, script_name)
66
81
  end
67
82
  end
68
83
 
69
- def check_forbidden
70
- return unless @path_info.include? ".."
84
+ def check_bad_request(path_info)
85
+ return if Utils.valid_path?(path_info)
86
+
87
+ body = "Bad Request\n"
88
+ size = body.bytesize
89
+ return [400, {CONTENT_TYPE => "text/plain",
90
+ CONTENT_LENGTH => size.to_s,
91
+ "X-Cascade" => "pass"}, [body]]
92
+ end
93
+
94
+ def check_forbidden(path_info)
95
+ return unless path_info.include? ".."
71
96
 
72
97
  body = "Forbidden\n"
73
- size = Rack::Utils.bytesize(body)
74
- return [403, {"Content-Type" => "text/plain",
98
+ size = body.bytesize
99
+ return [403, {CONTENT_TYPE => "text/plain",
75
100
  CONTENT_LENGTH => size.to_s,
76
101
  "X-Cascade" => "pass"}, [body]]
77
102
  end
78
103
 
79
- def list_directory
80
- @files = [['../','Parent Directory','','','']]
81
- glob = F.join(@path, '*')
104
+ def list_directory(path_info, path, script_name)
105
+ files = [['../','Parent Directory','','','']]
106
+ glob = ::File.join(path, '*')
82
107
 
83
- url_head = (@script_name.split('/') + @path_info.split('/')).map do |part|
84
- Rack::Utils.escape part
108
+ url_head = (script_name.split('/') + path_info.split('/')).map do |part|
109
+ Rack::Utils.escape_path part
85
110
  end
86
111
 
87
112
  Dir[glob].sort.each do |node|
88
113
  stat = stat(node)
89
- next unless stat
90
- basename = F.basename(node)
91
- ext = F.extname(node)
114
+ next unless stat
115
+ basename = ::File.basename(node)
116
+ ext = ::File.extname(node)
92
117
 
93
- url = F.join(*url_head + [Rack::Utils.escape(basename)])
118
+ url = ::File.join(*url_head + [Rack::Utils.escape_path(basename)])
94
119
  size = stat.size
95
120
  type = stat.directory? ? 'directory' : Mime.mime_type(ext)
96
121
  size = stat.directory? ? '-' : filesize_format(size)
@@ -98,49 +123,42 @@ table { width:100%%; }
98
123
  url << '/' if stat.directory?
99
124
  basename << '/' if stat.directory?
100
125
 
101
- @files << [ url, basename, size, type, mtime ]
126
+ files << [ url, basename, size, type, mtime ]
102
127
  end
103
128
 
104
- return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, self ]
129
+ return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, DirectoryBody.new(@root, path, files) ]
105
130
  end
106
131
 
107
- def stat(node, max = 10)
108
- F.stat(node)
132
+ def stat(node)
133
+ ::File.stat(node)
109
134
  rescue Errno::ENOENT, Errno::ELOOP
110
135
  return nil
111
136
  end
112
137
 
113
138
  # TODO: add correct response if not readable, not sure if 404 is the best
114
139
  # option
115
- def list_path
116
- @stat = F.stat(@path)
140
+ def list_path(env, path, path_info, script_name)
141
+ stat = ::File.stat(path)
117
142
 
118
- if @stat.readable?
119
- return @app.call(@env) if @stat.file?
120
- return list_directory if @stat.directory?
143
+ if stat.readable?
144
+ return @app.call(env) if stat.file?
145
+ return list_directory(path_info, path, script_name) if stat.directory?
121
146
  else
122
147
  raise Errno::ENOENT, 'No such file or directory'
123
148
  end
124
149
 
125
150
  rescue Errno::ENOENT, Errno::ELOOP
126
- return entity_not_found
151
+ return entity_not_found(path_info)
127
152
  end
128
153
 
129
- def entity_not_found
130
- body = "Entity not found: #{@path_info}\n"
131
- size = Rack::Utils.bytesize(body)
132
- return [404, {"Content-Type" => "text/plain",
154
+ def entity_not_found(path_info)
155
+ body = "Entity not found: #{path_info}\n"
156
+ size = body.bytesize
157
+ return [404, {CONTENT_TYPE => "text/plain",
133
158
  CONTENT_LENGTH => size.to_s,
134
159
  "X-Cascade" => "pass"}, [body]]
135
160
  end
136
161
 
137
- def each
138
- show_path = Rack::Utils.escape_html(@path.sub(/^#{@root}/,''))
139
- files = @files.map{|f| DIR_FILE % DIR_FILE_escape(*f) }*"\n"
140
- page = DIR_PAGE % [ show_path, show_path , files ]
141
- page.each_line{|l| yield l }
142
- end
143
-
144
162
  # Stolen from Ramaze
145
163
 
146
164
  FILESIZE_FORMAT = [
@@ -155,13 +173,7 @@ table { width:100%%; }
155
173
  return format % (int.to_f / size) if int >= size
156
174
  end
157
175
 
158
- int.to_s + 'B'
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) }]
176
+ "#{int}B"
165
177
  end
166
178
  end
167
179
  end
data/lib/rack/etag.rb CHANGED
@@ -1,4 +1,5 @@
1
- require 'digest/md5'
1
+ require 'rack'
2
+ require 'digest/sha2'
2
3
 
3
4
  module Rack
4
5
  # Automatically sets the ETag header on all String bodies.
@@ -11,7 +12,7 @@ module Rack
11
12
  # used when Etag is absent and a directive when it is present. The first
12
13
  # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
13
14
  class ETag
14
- ETAG_STRING = 'ETag'.freeze
15
+ ETAG_STRING = Rack::ETAG
15
16
  DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
16
17
 
17
18
  def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
@@ -64,10 +65,10 @@ module Rack
64
65
 
65
66
  body.each do |part|
66
67
  parts << part
67
- (digest ||= Digest::MD5.new) << part unless part.empty?
68
+ (digest ||= Digest::SHA256.new) << part unless part.empty?
68
69
  end
69
70
 
70
- [digest && digest.hexdigest, parts]
71
+ [digest && digest.hexdigest.byteslice(0, 32), parts]
71
72
  end
72
73
  end
73
74
  end
@@ -0,0 +1,154 @@
1
+ require 'rack/response'
2
+ require 'rack/body_proxy'
3
+
4
+ module Rack
5
+ ### This middleware provides hooks to certain places in the request /
6
+ #response lifecycle. This is so that middleware that don't need to filter
7
+ #the response data can safely leave it alone and not have to send messages
8
+ #down the traditional "rack stack".
9
+ #
10
+ # The events are:
11
+ #
12
+ # * on_start(request, response)
13
+ #
14
+ # This event is sent at the start of the request, before the next
15
+ # middleware in the chain is called. This method is called with a request
16
+ # object, and a response object. Right now, the response object is always
17
+ # nil, but in the future it may actually be a real response object.
18
+ #
19
+ # * on_commit(request, response)
20
+ #
21
+ # The response has been committed. The application has returned, but the
22
+ # response has not been sent to the webserver yet. This method is always
23
+ # called with a request object and the response object. The response
24
+ # object is constructed from the rack triple that the application returned.
25
+ # Changes may still be made to the response object at this point.
26
+ #
27
+ # * on_send(request, response)
28
+ #
29
+ # The webserver has started iterating over the response body and presumably
30
+ # has started sending data over the wire. This method is always called with
31
+ # a request object and the response object. The response object is
32
+ # constructed from the rack triple that the application returned. Changes
33
+ # SHOULD NOT be made to the response object as the webserver has already
34
+ # started sending data. Any mutations will likely result in an exception.
35
+ #
36
+ # * on_finish(request, response)
37
+ #
38
+ # The webserver has closed the response, and all data has been written to
39
+ # the response socket. The request and response object should both be
40
+ # read-only at this point. The body MAY NOT be available on the response
41
+ # object as it may have been flushed to the socket.
42
+ #
43
+ # * on_error(request, response, error)
44
+ #
45
+ # An exception has occurred in the application or an `on_commit` event.
46
+ # This method will get the request, the response (if available) and the
47
+ # exception that was raised.
48
+ #
49
+ # ## Order
50
+ #
51
+ # `on_start` is called on the handlers in the order that they were passed to
52
+ # the constructor. `on_commit`, on_send`, `on_finish`, and `on_error` are
53
+ # called in the reverse order. `on_finish` handlers are called inside an
54
+ # `ensure` block, so they are guaranteed to be called even if something
55
+ # raises an exception. If something raises an exception in a `on_finish`
56
+ # method, then nothing is guaranteed.
57
+
58
+ class Events
59
+ module Abstract
60
+ def on_start req, res
61
+ end
62
+
63
+ def on_commit req, res
64
+ end
65
+
66
+ def on_send req, res
67
+ end
68
+
69
+ def on_finish req, res
70
+ end
71
+
72
+ def on_error req, res, e
73
+ end
74
+ end
75
+
76
+ class EventedBodyProxy < Rack::BodyProxy # :nodoc:
77
+ attr_reader :request, :response
78
+
79
+ def initialize body, request, response, handlers, &block
80
+ super(body, &block)
81
+ @request = request
82
+ @response = response
83
+ @handlers = handlers
84
+ end
85
+
86
+ def each
87
+ @handlers.reverse_each { |handler| handler.on_send request, response }
88
+ super
89
+ end
90
+ end
91
+
92
+ class BufferedResponse < Rack::Response::Raw # :nodoc:
93
+ attr_reader :body
94
+
95
+ def initialize status, headers, body
96
+ super(status, headers)
97
+ @body = body
98
+ end
99
+
100
+ def to_a; [status, headers, body]; end
101
+ end
102
+
103
+ def initialize app, handlers
104
+ @app = app
105
+ @handlers = handlers
106
+ end
107
+
108
+ def call env
109
+ request = make_request env
110
+ on_start request, nil
111
+
112
+ begin
113
+ status, headers, body = @app.call request.env
114
+ response = make_response status, headers, body
115
+ on_commit request, response
116
+ rescue StandardError => e
117
+ on_error request, response, e
118
+ on_finish request, response
119
+ raise
120
+ end
121
+
122
+ body = EventedBodyProxy.new(body, request, response, @handlers) do
123
+ on_finish request, response
124
+ end
125
+ [response.status, response.headers, body]
126
+ end
127
+
128
+ private
129
+
130
+ def on_error request, response, e
131
+ @handlers.reverse_each { |handler| handler.on_error request, response, e }
132
+ end
133
+
134
+ def on_commit request, response
135
+ @handlers.reverse_each { |handler| handler.on_commit request, response }
136
+ end
137
+
138
+ def on_start request, response
139
+ @handlers.each { |handler| handler.on_start request, nil }
140
+ end
141
+
142
+ def on_finish request, response
143
+ @handlers.reverse_each { |handler| handler.on_finish request, response }
144
+ end
145
+
146
+ def make_request env
147
+ Rack::Request.new env
148
+ end
149
+
150
+ def make_response status, headers, body
151
+ BufferedResponse.new status, headers, body
152
+ end
153
+ end
154
+ end
data/lib/rack/file.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'time'
2
2
  require 'rack/utils'
3
3
  require 'rack/mime'
4
+ require 'rack/request'
5
+ require 'rack/head'
4
6
 
5
7
  module Rack
6
8
  # Rack::File serves files below the +root+ directory given, according to the
@@ -15,70 +17,70 @@ module Rack
15
17
  ALLOWED_VERBS = %w[GET HEAD OPTIONS]
16
18
  ALLOW_HEADER = ALLOWED_VERBS.join(', ')
17
19
 
18
- attr_accessor :root
19
- attr_accessor :path
20
- attr_accessor :cache_control
21
-
22
- alias :to_path :path
20
+ attr_reader :root
23
21
 
24
22
  def initialize(root, headers={}, default_mime = 'text/plain')
25
23
  @root = root
26
24
  @headers = headers
27
25
  @default_mime = default_mime
26
+ @head = Rack::Head.new(lambda { |env| get env })
28
27
  end
29
28
 
30
29
  def call(env)
31
- dup._call(env)
30
+ # HEAD requests drop the response body, including 4xx error messages.
31
+ @head.call env
32
32
  end
33
33
 
34
- F = ::File
35
-
36
- def _call(env)
37
- unless ALLOWED_VERBS.include? env[REQUEST_METHOD]
34
+ def get(env)
35
+ request = Rack::Request.new env
36
+ unless ALLOWED_VERBS.include? request.request_method
38
37
  return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
39
38
  end
40
39
 
41
- path_info = Utils.unescape(env[PATH_INFO])
42
- clean_path_info = Utils.clean_path_info(path_info)
40
+ path_info = Utils.unescape_path request.path_info
41
+ return fail(400, "Bad Request") unless Utils.valid_path?(path_info)
43
42
 
44
- @path = F.join(@root, clean_path_info)
43
+ clean_path_info = Utils.clean_path_info(path_info)
44
+ path = ::File.join(@root, clean_path_info)
45
45
 
46
46
  available = begin
47
- F.file?(@path) && F.readable?(@path)
47
+ ::File.file?(path) && ::File.readable?(path)
48
48
  rescue SystemCallError
49
49
  false
50
50
  end
51
51
 
52
52
  if available
53
- serving(env)
53
+ serving(request, path)
54
54
  else
55
55
  fail(404, "File not found: #{path_info}")
56
56
  end
57
57
  end
58
58
 
59
- def serving(env)
60
- if env["REQUEST_METHOD"] == "OPTIONS"
59
+ def serving(request, path)
60
+ if request.options?
61
61
  return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []]
62
62
  end
63
- last_modified = F.mtime(@path).httpdate
64
- return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
63
+ last_modified = ::File.mtime(path).httpdate
64
+ return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
65
65
 
66
66
  headers = { "Last-Modified" => last_modified }
67
+ mime_type = mime_type path, @default_mime
67
68
  headers[CONTENT_TYPE] = mime_type if mime_type
68
69
 
69
70
  # Set custom headers
70
71
  @headers.each { |field, content| headers[field] = content } if @headers
71
72
 
72
- response = [ 200, headers, env[REQUEST_METHOD] == "HEAD" ? [] : self ]
73
+ response = [ 200, headers ]
73
74
 
74
- size = filesize
75
+ size = filesize path
75
76
 
76
- ranges = Rack::Utils.byte_ranges(env, size)
77
+ range = nil
78
+ ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
77
79
  if ranges.nil? || ranges.length > 1
78
80
  # No ranges, or multiple ranges (which we don't support):
79
81
  # TODO: Support multiple byte-ranges
80
82
  response[0] = 200
81
- @range = 0..size-1
83
+ range = 0..size-1
82
84
  elsif ranges.empty?
83
85
  # Unsatisfiable. Return error, and file size:
84
86
  response = fail(416, "Byte range unsatisfiable")
@@ -86,36 +88,58 @@ module Rack
86
88
  return response
87
89
  else
88
90
  # Partial content:
89
- @range = ranges[0]
91
+ range = ranges[0]
90
92
  response[0] = 206
91
- response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
92
- size = @range.end - @range.begin + 1
93
+ response[1]["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
94
+ size = range.end - range.begin + 1
93
95
  end
94
96
 
95
97
  response[2] = [response_body] unless response_body.nil?
96
98
 
97
99
  response[1][CONTENT_LENGTH] = size.to_s
100
+ response[2] = make_body request, path, range
98
101
  response
99
102
  end
100
103
 
101
- def each
102
- F.open(@path, "rb") do |file|
103
- file.seek(@range.begin)
104
- remaining_len = @range.end-@range.begin+1
105
- while remaining_len > 0
106
- part = file.read([8192, remaining_len].min)
107
- break unless part
108
- remaining_len -= part.length
104
+ class Iterator
105
+ attr_reader :path, :range
106
+ alias :to_path :path
107
+
108
+ def initialize path, range
109
+ @path = path
110
+ @range = range
111
+ end
109
112
 
110
- yield part
113
+ def each
114
+ ::File.open(path, "rb") do |file|
115
+ file.seek(range.begin)
116
+ remaining_len = range.end-range.begin+1
117
+ while remaining_len > 0
118
+ part = file.read([8192, remaining_len].min)
119
+ break unless part
120
+ remaining_len -= part.length
121
+
122
+ yield part
123
+ end
111
124
  end
112
125
  end
126
+
127
+ def close; end
113
128
  end
114
129
 
115
130
  private
116
131
 
132
+ def make_body request, path, range
133
+ if request.head?
134
+ []
135
+ else
136
+ Iterator.new path, range
137
+ end
138
+ end
139
+
117
140
  def fail(status, body, headers = {})
118
141
  body += "\n"
142
+
119
143
  [
120
144
  status,
121
145
  {
@@ -128,18 +152,18 @@ module Rack
128
152
  end
129
153
 
130
154
  # The MIME type for the contents of the file located at @path
131
- def mime_type
132
- Mime.mime_type(F.extname(@path), @default_mime)
155
+ def mime_type path, default_mime
156
+ Mime.mime_type(::File.extname(path), default_mime)
133
157
  end
134
158
 
135
- def filesize
159
+ def filesize path
136
160
  # If response_body is present, use its size.
137
- return Rack::Utils.bytesize(response_body) if response_body
161
+ return response_body.bytesize if response_body
138
162
 
139
163
  # We check via File::size? whether this file provides size info
140
164
  # via stat (e.g. /proc files often don't), otherwise we have to
141
165
  # figure it out by reading the whole file into memory.
142
- F.size?(@path) || Utils.bytesize(F.read(@path))
166
+ ::File.size?(path) || ::File.read(path).bytesize
143
167
  end
144
168
 
145
169
  # By default, the response body for file requests is nil.