rack 1.1.6 → 1.6.9

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.
Files changed (212) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +1 -1
  3. data/HISTORY.md +375 -0
  4. data/KNOWN-ISSUES +23 -0
  5. data/README.rdoc +312 -0
  6. data/Rakefile +124 -0
  7. data/SPEC +125 -32
  8. data/contrib/rack.png +0 -0
  9. data/contrib/rack.svg +150 -0
  10. data/contrib/rack_logo.svg +1 -1
  11. data/contrib/rdoc.css +412 -0
  12. data/example/protectedlobster.rb +1 -1
  13. data/lib/rack/auth/abstract/handler.rb +4 -4
  14. data/lib/rack/auth/abstract/request.rb +7 -5
  15. data/lib/rack/auth/basic.rb +1 -1
  16. data/lib/rack/auth/digest/md5.rb +7 -3
  17. data/lib/rack/auth/digest/nonce.rb +1 -1
  18. data/lib/rack/auth/digest/params.rb +7 -9
  19. data/lib/rack/auth/digest/request.rb +10 -9
  20. data/lib/rack/backports/uri/common_18.rb +56 -0
  21. data/lib/rack/backports/uri/common_192.rb +52 -0
  22. data/lib/rack/backports/uri/common_193.rb +29 -0
  23. data/lib/rack/body_proxy.rb +39 -0
  24. data/lib/rack/builder.rb +106 -22
  25. data/lib/rack/cascade.rb +17 -6
  26. data/lib/rack/chunked.rb +44 -24
  27. data/lib/rack/commonlogger.rb +36 -13
  28. data/lib/rack/conditionalget.rb +49 -17
  29. data/lib/rack/config.rb +5 -0
  30. data/lib/rack/content_length.rb +14 -6
  31. data/lib/rack/content_type.rb +7 -1
  32. data/lib/rack/deflater.rb +73 -15
  33. data/lib/rack/directory.rb +18 -8
  34. data/lib/rack/etag.rb +59 -9
  35. data/lib/rack/file.rb +106 -44
  36. data/lib/rack/handler/cgi.rb +11 -11
  37. data/lib/rack/handler/fastcgi.rb +18 -6
  38. data/lib/rack/handler/lsws.rb +2 -4
  39. data/lib/rack/handler/mongrel.rb +22 -6
  40. data/lib/rack/handler/scgi.rb +16 -8
  41. data/lib/rack/handler/thin.rb +19 -4
  42. data/lib/rack/handler/webrick.rb +72 -19
  43. data/lib/rack/handler.rb +47 -14
  44. data/lib/rack/head.rb +10 -2
  45. data/lib/rack/lint.rb +260 -75
  46. data/lib/rack/lobster.rb +13 -8
  47. data/lib/rack/lock.rb +13 -3
  48. data/lib/rack/logger.rb +0 -2
  49. data/lib/rack/methodoverride.rb +27 -8
  50. data/lib/rack/mime.rb +625 -167
  51. data/lib/rack/mock.rb +78 -53
  52. data/lib/rack/multipart/generator.rb +93 -0
  53. data/lib/rack/multipart/parser.rb +253 -0
  54. data/lib/rack/multipart/uploaded_file.rb +34 -0
  55. data/lib/rack/multipart.rb +34 -0
  56. data/lib/rack/nulllogger.rb +21 -2
  57. data/lib/rack/recursive.rb +10 -5
  58. data/lib/rack/reloader.rb +3 -2
  59. data/lib/rack/request.rb +201 -74
  60. data/lib/rack/response.rb +41 -28
  61. data/lib/rack/rewindable_input.rb +15 -11
  62. data/lib/rack/runtime.rb +16 -3
  63. data/lib/rack/sendfile.rb +47 -29
  64. data/lib/rack/server.rb +223 -47
  65. data/lib/rack/session/abstract/id.rb +289 -30
  66. data/lib/rack/session/cookie.rb +133 -44
  67. data/lib/rack/session/memcache.rb +30 -56
  68. data/lib/rack/session/pool.rb +19 -43
  69. data/lib/rack/showexceptions.rb +53 -15
  70. data/lib/rack/showstatus.rb +14 -7
  71. data/lib/rack/static.rb +124 -12
  72. data/lib/rack/tempfile_reaper.rb +22 -0
  73. data/lib/rack/urlmap.rb +49 -15
  74. data/lib/rack/utils/okjson.rb +600 -0
  75. data/lib/rack/utils.rb +363 -361
  76. data/lib/rack.rb +17 -23
  77. data/rack.gemspec +11 -20
  78. data/test/builder/anything.rb +5 -0
  79. data/test/builder/comment.ru +4 -0
  80. data/test/builder/end.ru +5 -0
  81. data/test/builder/line.ru +1 -0
  82. data/test/builder/options.ru +2 -0
  83. data/test/cgi/assets/folder/test.js +1 -0
  84. data/test/cgi/assets/fonts/font.eot +1 -0
  85. data/test/cgi/assets/images/image.png +1 -0
  86. data/test/cgi/assets/index.html +1 -0
  87. data/test/cgi/assets/javascripts/app.js +1 -0
  88. data/test/cgi/assets/stylesheets/app.css +1 -0
  89. data/test/cgi/lighttpd.conf +26 -0
  90. data/test/cgi/rackup_stub.rb +6 -0
  91. data/test/cgi/sample_rackup.ru +5 -0
  92. data/test/cgi/test +9 -0
  93. data/test/cgi/test+directory/test+file +1 -0
  94. data/test/cgi/test.fcgi +8 -0
  95. data/test/cgi/test.ru +5 -0
  96. data/test/gemloader.rb +10 -0
  97. data/test/multipart/bad_robots +259 -0
  98. data/test/multipart/binary +0 -0
  99. data/test/multipart/content_type_and_no_filename +6 -0
  100. data/test/multipart/empty +10 -0
  101. data/test/multipart/fail_16384_nofile +814 -0
  102. data/test/multipart/file1.txt +1 -0
  103. data/test/multipart/filename_and_modification_param +7 -0
  104. data/test/multipart/filename_and_no_name +6 -0
  105. data/test/multipart/filename_with_escaped_quotes +6 -0
  106. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  107. data/test/multipart/filename_with_null_byte +7 -0
  108. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  109. data/test/multipart/filename_with_unescaped_percentages +6 -0
  110. data/test/multipart/filename_with_unescaped_percentages2 +6 -0
  111. data/test/multipart/filename_with_unescaped_percentages3 +6 -0
  112. data/test/multipart/filename_with_unescaped_quotes +6 -0
  113. data/test/multipart/ie +6 -0
  114. data/test/multipart/invalid_character +6 -0
  115. data/test/multipart/mixed_files +21 -0
  116. data/test/multipart/nested +10 -0
  117. data/test/multipart/none +9 -0
  118. data/test/multipart/semicolon +6 -0
  119. data/test/multipart/text +15 -0
  120. data/test/multipart/three_files_three_fields +31 -0
  121. data/test/multipart/webkit +32 -0
  122. data/test/rackup/config.ru +31 -0
  123. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  124. data/test/{spec_rack_auth_basic.rb → spec_auth_basic.rb} +23 -15
  125. data/test/{spec_rack_auth_digest.rb → spec_auth_digest.rb} +56 -29
  126. data/test/spec_body_proxy.rb +85 -0
  127. data/test/spec_builder.rb +223 -0
  128. data/test/{spec_rack_cascade.rb → spec_cascade.rb} +28 -15
  129. data/test/{spec_rack_cgi.rb → spec_cgi.rb} +44 -31
  130. data/test/spec_chunked.rb +101 -0
  131. data/test/spec_commonlogger.rb +93 -0
  132. data/test/spec_conditionalget.rb +102 -0
  133. data/test/{spec_rack_config.rb → spec_config.rb} +6 -8
  134. data/test/spec_content_length.rb +85 -0
  135. data/test/spec_content_type.rb +45 -0
  136. data/test/spec_deflater.rb +339 -0
  137. data/test/{spec_rack_directory.rb → spec_directory.rb} +37 -10
  138. data/test/spec_etag.rb +107 -0
  139. data/test/{spec_rack_fastcgi.rb → spec_fastcgi.rb} +47 -29
  140. data/test/spec_file.rb +221 -0
  141. data/test/spec_handler.rb +72 -0
  142. data/test/spec_head.rb +45 -0
  143. data/test/{spec_rack_lint.rb → spec_lint.rb} +82 -60
  144. data/test/spec_lobster.rb +58 -0
  145. data/test/spec_lock.rb +164 -0
  146. data/test/spec_logger.rb +23 -0
  147. data/test/spec_methodoverride.rb +95 -0
  148. data/test/spec_mime.rb +51 -0
  149. data/test/{spec_rack_mock.rb → spec_mock.rb} +92 -38
  150. data/test/{spec_rack_mongrel.rb → spec_mongrel.rb} +46 -53
  151. data/test/spec_multipart.rb +600 -0
  152. data/test/spec_nulllogger.rb +20 -0
  153. data/test/spec_recursive.rb +72 -0
  154. data/test/spec_request.rb +1227 -0
  155. data/test/spec_response.rb +407 -0
  156. data/test/spec_rewindable_input.rb +118 -0
  157. data/test/spec_runtime.rb +49 -0
  158. data/test/spec_sendfile.rb +130 -0
  159. data/test/spec_server.rb +167 -0
  160. data/test/spec_session_abstract_id.rb +53 -0
  161. data/test/spec_session_cookie.rb +410 -0
  162. data/test/{spec_rack_session_memcache.rb → spec_session_memcache.rb} +119 -71
  163. data/test/{spec_rack_session_pool.rb → spec_session_pool.rb} +106 -69
  164. data/test/spec_showexceptions.rb +85 -0
  165. data/test/spec_showstatus.rb +103 -0
  166. data/test/spec_static.rb +145 -0
  167. data/test/spec_tempfile_reaper.rb +63 -0
  168. data/test/{spec_rack_thin.rb → spec_thin.rb} +35 -35
  169. data/test/{spec_rack_urlmap.rb → spec_urlmap.rb} +40 -19
  170. data/test/spec_utils.rb +647 -0
  171. data/test/spec_version.rb +17 -0
  172. data/test/spec_webrick.rb +184 -0
  173. data/test/static/another/index.html +1 -0
  174. data/test/static/index.html +1 -0
  175. data/test/testrequest.rb +78 -0
  176. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  177. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  178. metadata +220 -239
  179. data/RDOX +0 -0
  180. data/README +0 -592
  181. data/lib/rack/adapter/camping.rb +0 -22
  182. data/test/spec_auth.rb +0 -57
  183. data/test/spec_rack_builder.rb +0 -84
  184. data/test/spec_rack_camping.rb +0 -55
  185. data/test/spec_rack_chunked.rb +0 -62
  186. data/test/spec_rack_commonlogger.rb +0 -61
  187. data/test/spec_rack_conditionalget.rb +0 -41
  188. data/test/spec_rack_content_length.rb +0 -43
  189. data/test/spec_rack_content_type.rb +0 -30
  190. data/test/spec_rack_deflater.rb +0 -127
  191. data/test/spec_rack_etag.rb +0 -17
  192. data/test/spec_rack_file.rb +0 -75
  193. data/test/spec_rack_handler.rb +0 -43
  194. data/test/spec_rack_head.rb +0 -30
  195. data/test/spec_rack_lobster.rb +0 -45
  196. data/test/spec_rack_lock.rb +0 -38
  197. data/test/spec_rack_logger.rb +0 -21
  198. data/test/spec_rack_methodoverride.rb +0 -60
  199. data/test/spec_rack_nulllogger.rb +0 -13
  200. data/test/spec_rack_recursive.rb +0 -77
  201. data/test/spec_rack_request.rb +0 -594
  202. data/test/spec_rack_response.rb +0 -221
  203. data/test/spec_rack_rewindable_input.rb +0 -118
  204. data/test/spec_rack_runtime.rb +0 -35
  205. data/test/spec_rack_sendfile.rb +0 -86
  206. data/test/spec_rack_session_cookie.rb +0 -92
  207. data/test/spec_rack_showexceptions.rb +0 -21
  208. data/test/spec_rack_showstatus.rb +0 -72
  209. data/test/spec_rack_static.rb +0 -37
  210. data/test/spec_rack_utils.rb +0 -557
  211. data/test/spec_rack_webrick.rb +0 -130
  212. data/test/spec_rackup.rb +0 -164
@@ -13,35 +13,67 @@ module Rack
13
13
  # a conditional GET matches.
14
14
  #
15
15
  # Adapted from Michael Klishin's Merb implementation:
16
- # http://github.com/wycats/merb-core/tree/master/lib/merb-core/rack/middleware/conditional_get.rb
16
+ # https://github.com/wycats/merb/blob/master/merb-core/lib/merb-core/rack/middleware/conditional_get.rb
17
17
  class ConditionalGet
18
18
  def initialize(app)
19
19
  @app = app
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
+ original_body = body
32
+ body = Rack::BodyProxy.new([]) do
33
+ original_body.close if original_body.respond_to?(:close)
34
+ end
35
+ end
36
+ [status, headers, body]
37
+ else
38
+ @app.call(env)
32
39
  end
33
- [status, headers, body]
34
40
  end
35
41
 
36
42
  private
37
- def etag_matches?(env, headers)
38
- etag = headers['Etag'] and etag == env['HTTP_IF_NONE_MATCH']
43
+
44
+ def fresh?(env, headers)
45
+ modified_since = env['HTTP_IF_MODIFIED_SINCE']
46
+ none_match = env['HTTP_IF_NONE_MATCH']
47
+
48
+ return false unless modified_since || none_match
49
+
50
+ success = true
51
+ success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
52
+ success &&= etag_matches?(none_match, headers) if none_match
53
+ success
39
54
  end
40
55
 
41
- def modified_since?(env, headers)
42
- last_modified = headers['Last-Modified'] and
43
- last_modified == env['HTTP_IF_MODIFIED_SINCE']
56
+ def etag_matches?(none_match, headers)
57
+ etag = headers['ETag'] and etag == none_match
44
58
  end
45
- end
46
59
 
60
+ def modified_since?(modified_since, headers)
61
+ last_modified = to_rfc2822(headers['Last-Modified']) and
62
+ modified_since and
63
+ modified_since >= last_modified
64
+ end
65
+
66
+ def to_rfc2822(since)
67
+ # shortest possible valid date is the obsolete: 1 Nov 97 09:55 A
68
+ # anything shorter is invalid, this avoids exceptions for common cases
69
+ # most common being the empty string
70
+ if since && since.length >= 16
71
+ # NOTE: there is no trivial way to write this in a non execption way
72
+ # _rfc2822 returns a hash but is not that usable
73
+ Time.rfc2822(since) rescue nil
74
+ else
75
+ nil
76
+ end
77
+ end
78
+ end
47
79
  end
data/lib/rack/config.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  module Rack
2
2
  # Rack::Config modifies the environment using the block given during
3
3
  # initialization.
4
+ #
5
+ # Example:
6
+ # use Rack::Config do |env|
7
+ # env['my-key'] = 'some-value'
8
+ # end
4
9
  class Config
5
10
  def initialize(app, &block)
6
11
  @app = app
@@ -1,6 +1,8 @@
1
1
  require 'rack/utils'
2
+ require 'rack/body_proxy'
2
3
 
3
4
  module Rack
5
+
4
6
  # Sets the Content-Length header on responses with fixed-length bodies.
5
7
  class ContentLength
6
8
  include Rack::Utils
@@ -13,14 +15,20 @@ module Rack
13
15
  status, headers, body = @app.call(env)
14
16
  headers = HeaderHash.new(headers)
15
17
 
16
- if !STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
17
- !headers['Content-Length'] &&
18
+ if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
19
+ !headers[CONTENT_LENGTH] &&
18
20
  !headers['Transfer-Encoding'] &&
19
- (body.respond_to?(:to_ary) || body.respond_to?(:to_str))
21
+ body.respond_to?(:to_ary)
22
+
23
+ obody = body
24
+ body, length = [], 0
25
+ obody.each { |part| body << part; length += bytesize(part) }
26
+
27
+ body = BodyProxy.new(body) do
28
+ obody.close if obody.respond_to?(:close)
29
+ end
20
30
 
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) }
23
- headers['Content-Length'] = length.to_s
31
+ headers[CONTENT_LENGTH] = length.to_s
24
32
  end
25
33
 
26
34
  [status, headers, body]
@@ -9,6 +9,8 @@ module Rack
9
9
  #
10
10
  # When no content type argument is provided, "text/html" is assumed.
11
11
  class ContentType
12
+ include Rack::Utils
13
+
12
14
  def initialize(app, content_type = "text/html")
13
15
  @app, @content_type = app, content_type
14
16
  end
@@ -16,7 +18,11 @@ module Rack
16
18
  def call(env)
17
19
  status, headers, body = @app.call(env)
18
20
  headers = Utils::HeaderHash.new(headers)
19
- headers['Content-Type'] ||= @content_type
21
+
22
+ unless STATUS_WITH_NO_ENTITY_BODY.include?(status)
23
+ headers[CONTENT_TYPE] ||= @content_type
24
+ end
25
+
20
26
  [status, headers, body]
21
27
  end
22
28
  end
data/lib/rack/deflater.rb CHANGED
@@ -1,22 +1,41 @@
1
1
  require "zlib"
2
- require "stringio"
3
2
  require "time" # for Time.httpdate
4
3
  require 'rack/utils'
5
4
 
6
5
  module Rack
6
+ # This middleware enables compression of http responses.
7
+ #
8
+ # Currently supported compression algorithms:
9
+ #
10
+ # * gzip
11
+ # * deflate
12
+ # * identity (no transformation)
13
+ #
14
+ # The middleware automatically detects when compression is supported
15
+ # and allowed. For example no transformation is made when a cache
16
+ # directive of 'no-transform' is present, or when the response status
17
+ # code is one that doesn't allow an entity body.
7
18
  class Deflater
8
- def initialize(app)
19
+ ##
20
+ # Creates Rack::Deflater middleware.
21
+ #
22
+ # [app] rack app instance
23
+ # [options] hash of deflater options, i.e.
24
+ # '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 }
26
+ # 'include' - a list of content types that should be compressed
27
+ def initialize(app, options = {})
9
28
  @app = app
29
+
30
+ @condition = options[:if]
31
+ @compressible_types = options[:include]
10
32
  end
11
33
 
12
34
  def call(env)
13
35
  status, headers, body = @app.call(env)
14
36
  headers = Utils::HeaderHash.new(headers)
15
37
 
16
- # Skip compressing empty entity body responses and responses with
17
- # no-transform set.
18
- if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
19
- headers['Cache-Control'].to_s =~ /\bno-transform\b/
38
+ unless should_deflate?(env, status, headers, body)
20
39
  return [status, headers, body]
21
40
  end
22
41
 
@@ -34,19 +53,20 @@ module Rack
34
53
  case encoding
35
54
  when "gzip"
36
55
  headers['Content-Encoding'] = "gzip"
37
- headers.delete('Content-Length')
56
+ headers.delete(CONTENT_LENGTH)
38
57
  mtime = headers.key?("Last-Modified") ?
39
58
  Time.httpdate(headers["Last-Modified"]) : Time.now
40
59
  [status, headers, GzipStream.new(body, mtime)]
41
60
  when "deflate"
42
61
  headers['Content-Encoding'] = "deflate"
43
- headers.delete('Content-Length')
62
+ headers.delete(CONTENT_LENGTH)
44
63
  [status, headers, DeflateStream.new(body)]
45
64
  when "identity"
46
65
  [status, headers, body]
47
66
  when nil
48
67
  message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
49
- [406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]]
68
+ bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
69
+ [406, {CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s}, bp]
50
70
  end
51
71
  end
52
72
 
@@ -54,14 +74,18 @@ module Rack
54
74
  def initialize(body, mtime)
55
75
  @body = body
56
76
  @mtime = mtime
77
+ @closed = false
57
78
  end
58
79
 
59
80
  def each(&block)
60
81
  @writer = block
61
82
  gzip =::Zlib::GzipWriter.new(self)
62
83
  gzip.mtime = @mtime
63
- @body.each { |part| gzip.write(part) }
64
- @body.close if @body.respond_to?(:close)
84
+ @body.each { |part|
85
+ gzip.write(part)
86
+ gzip.flush
87
+ }
88
+ ensure
65
89
  gzip.close
66
90
  @writer = nil
67
91
  end
@@ -69,6 +93,12 @@ module Rack
69
93
  def write(data)
70
94
  @writer.call(data)
71
95
  end
96
+
97
+ def close
98
+ return if @closed
99
+ @closed = true
100
+ @body.close if @body.respond_to?(:close)
101
+ end
72
102
  end
73
103
 
74
104
  class DeflateStream
@@ -82,15 +112,43 @@ module Rack
82
112
 
83
113
  def initialize(body)
84
114
  @body = body
115
+ @closed = false
85
116
  end
86
117
 
87
118
  def each
88
- deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
89
- @body.each { |part| yield deflater.deflate(part) }
90
- @body.close if @body.respond_to?(:close)
91
- yield deflater.finish
119
+ deflator = ::Zlib::Deflate.new(*DEFLATE_ARGS)
120
+ @body.each { |part| yield deflator.deflate(part, Zlib::SYNC_FLUSH) }
121
+ yield deflator.finish
92
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)
93
131
  end
94
132
  end
133
+
134
+ private
135
+
136
+ def should_deflate?(env, status, headers, body)
137
+ # Skip compressing empty entity body responses and responses with
138
+ # no-transform set.
139
+ if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
140
+ headers[CACHE_CONTROL].to_s =~ /\bno-transform\b/ ||
141
+ (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
142
+ return false
143
+ end
144
+
145
+ # 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'][/[^;]*/]))
147
+
148
+ # Skip if @condition lambda is given and evaluates to false
149
+ return false if @condition && !@condition.call(env, status, headers, body)
150
+
151
+ true
152
+ end
95
153
  end
96
154
  end
@@ -55,8 +55,8 @@ table { width:100%%; }
55
55
 
56
56
  def _call(env)
57
57
  @env = env
58
- @script_name = env['SCRIPT_NAME']
59
- @path_info = Utils.unescape(env['PATH_INFO'])
58
+ @script_name = env[SCRIPT_NAME]
59
+ @path_info = Utils.unescape(env[PATH_INFO])
60
60
 
61
61
  if forbidden = check_forbidden
62
62
  forbidden
@@ -72,7 +72,7 @@ table { width:100%%; }
72
72
  body = "Forbidden\n"
73
73
  size = Rack::Utils.bytesize(body)
74
74
  return [403, {"Content-Type" => "text/plain",
75
- "Content-Length" => size.to_s,
75
+ CONTENT_LENGTH => size.to_s,
76
76
  "X-Cascade" => "pass"}, [body]]
77
77
  end
78
78
 
@@ -80,13 +80,17 @@ table { width:100%%; }
80
80
  @files = [['../','Parent Directory','','','']]
81
81
  glob = F.join(@path, '*')
82
82
 
83
+ url_head = (@script_name.split('/') + @path_info.split('/')).map do |part|
84
+ Rack::Utils.escape part
85
+ end
86
+
83
87
  Dir[glob].sort.each do |node|
84
88
  stat = stat(node)
85
89
  next unless stat
86
90
  basename = F.basename(node)
87
91
  ext = F.extname(node)
88
92
 
89
- url = F.join(@script_name, @path_info, basename)
93
+ url = F.join(*url_head + [Rack::Utils.escape(basename)])
90
94
  size = stat.size
91
95
  type = stat.directory? ? 'directory' : Mime.mime_type(ext)
92
96
  size = stat.directory? ? '-' : filesize_format(size)
@@ -97,7 +101,7 @@ table { width:100%%; }
97
101
  @files << [ url, basename, size, type, mtime ]
98
102
  end
99
103
 
100
- return [ 200, {'Content-Type'=>'text/html; charset=utf-8'}, self ]
104
+ return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, self ]
101
105
  end
102
106
 
103
107
  def stat(node, max = 10)
@@ -126,13 +130,13 @@ table { width:100%%; }
126
130
  body = "Entity not found: #{@path_info}\n"
127
131
  size = Rack::Utils.bytesize(body)
128
132
  return [404, {"Content-Type" => "text/plain",
129
- "Content-Length" => size.to_s,
133
+ CONTENT_LENGTH => size.to_s,
130
134
  "X-Cascade" => "pass"}, [body]]
131
135
  end
132
136
 
133
137
  def each
134
- show_path = @path.sub(/^#{@root}/,'')
135
- 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"
136
140
  page = DIR_PAGE % [ show_path, show_path , files ]
137
141
  page.each_line{|l| yield l }
138
142
  end
@@ -153,5 +157,11 @@ table { width:100%%; }
153
157
 
154
158
  int.to_s + 'B'
155
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
156
166
  end
157
167
  end
data/lib/rack/etag.rb CHANGED
@@ -1,23 +1,73 @@
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, private, must-revalidate"
5
13
  class ETag
6
- def initialize(app)
14
+ ETAG_STRING = 'ETag'.freeze
15
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
16
+
17
+ def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
7
18
  @app = app
19
+ @cache_control = cache_control
20
+ @no_cache_control = no_cache_control
8
21
  end
9
22
 
10
23
  def call(env)
11
24
  status, headers, body = @app.call(env)
12
25
 
13
- if !headers.has_key?('ETag')
14
- parts = []
15
- body.each { |part| parts << part.to_s }
16
- headers['ETag'] = %("#{Digest::MD5.hexdigest(parts.join(""))}")
17
- [status, headers, parts]
18
- else
19
- [status, headers, body]
26
+ if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
27
+ original_body = body
28
+ digest, new_body = digest_body(body)
29
+ body = Rack::BodyProxy.new(new_body) do
30
+ original_body.close if original_body.respond_to?(:close)
31
+ end
32
+ headers[ETAG_STRING] = %(W/"#{digest}") if digest
20
33
  end
34
+
35
+ unless headers[CACHE_CONTROL]
36
+ if digest
37
+ headers[CACHE_CONTROL] = @cache_control if @cache_control
38
+ else
39
+ headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
40
+ end
41
+ end
42
+
43
+ [status, headers, body]
21
44
  end
45
+
46
+ private
47
+
48
+ def etag_status?(status)
49
+ status == 200 || status == 201
50
+ end
51
+
52
+ def etag_body?(body)
53
+ !body.respond_to?(:to_path)
54
+ end
55
+
56
+ def skip_caching?(headers)
57
+ (headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
58
+ headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
59
+ end
60
+
61
+ def digest_body(body)
62
+ parts = []
63
+ digest = nil
64
+
65
+ body.each do |part|
66
+ parts << part
67
+ (digest ||= Digest::MD5.new) << part unless part.empty?
68
+ end
69
+
70
+ [digest && digest.hexdigest, parts]
71
+ end
22
72
  end
23
73
  end
data/lib/rack/file.rb CHANGED
@@ -3,20 +3,28 @@ require 'rack/utils'
3
3
  require 'rack/mime'
4
4
 
5
5
  module Rack
6
- # Rack::File serves files below the +root+ given, according to the
6
+ # Rack::File serves files below the +root+ directory given, according to the
7
7
  # path info of the Rack request.
8
+ # e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
9
+ # as http://localhost:9292/passwd
8
10
  #
9
11
  # Handlers can detect if bodies are a Rack::File, and use mechanisms
10
12
  # like sendfile on the +path+.
11
13
 
12
14
  class File
15
+ ALLOWED_VERBS = %w[GET HEAD OPTIONS]
16
+ ALLOW_HEADER = ALLOWED_VERBS.join(', ')
17
+
13
18
  attr_accessor :root
14
19
  attr_accessor :path
20
+ attr_accessor :cache_control
15
21
 
16
22
  alias :to_path :path
17
23
 
18
- def initialize(root)
24
+ def initialize(root, headers={}, default_mime = 'text/plain')
19
25
  @root = root
26
+ @headers = headers
27
+ @default_mime = default_mime
20
28
  end
21
29
 
22
30
  def call(env)
@@ -26,65 +34,119 @@ module Rack
26
34
  F = ::File
27
35
 
28
36
  def _call(env)
29
- @path_info = Utils.unescape(env["PATH_INFO"])
30
- return forbidden if @path_info.include? ".."
37
+ unless ALLOWED_VERBS.include? env[REQUEST_METHOD]
38
+ return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
39
+ end
31
40
 
32
- @path = F.join(@root, @path_info)
41
+ path_info = Utils.unescape(env[PATH_INFO])
42
+ clean_path_info = Utils.clean_path_info(path_info)
33
43
 
34
- begin
35
- if F.file?(@path) && F.readable?(@path)
36
- serving
37
- else
38
- raise Errno::EPERM
39
- end
44
+ @path = F.join(@root, clean_path_info)
45
+
46
+ available = begin
47
+ F.file?(@path) && F.readable?(@path)
40
48
  rescue SystemCallError
41
- not_found
49
+ false
42
50
  end
43
- end
44
51
 
45
- def forbidden
46
- body = "Forbidden\n"
47
- [403, {"Content-Type" => "text/plain",
48
- "Content-Length" => body.size.to_s,
49
- "X-Cascade" => "pass"},
50
- [body]]
52
+ if available
53
+ serving(env)
54
+ else
55
+ fail(404, "File not found: #{path_info}")
56
+ end
51
57
  end
52
58
 
53
- # NOTE:
54
- # We check via File::size? whether this file provides size info
55
- # via stat (e.g. /proc files often don't), otherwise we have to
56
- # figure it out by reading the whole file into memory. And while
57
- # we're at it we also use this as body then.
59
+ def serving(env)
60
+ if env["REQUEST_METHOD"] == "OPTIONS"
61
+ return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []]
62
+ end
63
+ last_modified = F.mtime(@path).httpdate
64
+ return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
65
+
66
+ headers = { "Last-Modified" => last_modified }
67
+ headers[CONTENT_TYPE] = mime_type if mime_type
68
+
69
+ # Set custom headers
70
+ @headers.each { |field, content| headers[field] = content } if @headers
58
71
 
59
- def serving
60
- if size = F.size?(@path)
61
- body = self
72
+ response = [ 200, headers, env[REQUEST_METHOD] == "HEAD" ? [] : self ]
73
+
74
+ size = filesize
75
+
76
+ ranges = Rack::Utils.byte_ranges(env, size)
77
+ if ranges.nil? || ranges.length > 1
78
+ # No ranges, or multiple ranges (which we don't support):
79
+ # TODO: Support multiple byte-ranges
80
+ response[0] = 200
81
+ @range = 0..size-1
82
+ elsif ranges.empty?
83
+ # Unsatisfiable. Return error, and file size:
84
+ response = fail(416, "Byte range unsatisfiable")
85
+ response[1]["Content-Range"] = "bytes */#{size}"
86
+ return response
62
87
  else
63
- body = [F.read(@path)]
64
- size = Utils.bytesize(body.first)
88
+ # Partial content:
89
+ @range = ranges[0]
90
+ response[0] = 206
91
+ response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
92
+ size = @range.end - @range.begin + 1
65
93
  end
66
94
 
67
- [200, {
68
- "Last-Modified" => F.mtime(@path).httpdate,
69
- "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain'),
70
- "Content-Length" => size.to_s
71
- }, body]
72
- end
95
+ response[2] = [response_body] unless response_body.nil?
73
96
 
74
- def not_found
75
- body = "File not found: #{@path_info}\n"
76
- [404, {"Content-Type" => "text/plain",
77
- "Content-Length" => body.size.to_s,
78
- "X-Cascade" => "pass"},
79
- [body]]
97
+ response[1][CONTENT_LENGTH] = size.to_s
98
+ response
80
99
  end
81
100
 
82
101
  def each
83
- F.open(@path, "rb") { |file|
84
- while part = file.read(8192)
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
109
+
85
110
  yield part
86
111
  end
87
- }
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def fail(status, body, headers = {})
118
+ body += "\n"
119
+ [
120
+ status,
121
+ {
122
+ CONTENT_TYPE => "text/plain",
123
+ CONTENT_LENGTH => body.size.to_s,
124
+ "X-Cascade" => "pass"
125
+ }.merge!(headers),
126
+ [body]
127
+ ]
128
+ end
129
+
130
+ # 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)
133
+ end
134
+
135
+ def filesize
136
+ # If response_body is present, use its size.
137
+ return Rack::Utils.bytesize(response_body) if response_body
138
+
139
+ # We check via File::size? whether this file provides size info
140
+ # via stat (e.g. /proc files often don't), otherwise we have to
141
+ # figure it out by reading the whole file into memory.
142
+ F.size?(@path) || Utils.bytesize(F.read(@path))
143
+ end
144
+
145
+ # By default, the response body for file requests is nil.
146
+ # In this case, the response body will be generated later
147
+ # from the file at @path
148
+ def response_body
149
+ nil
88
150
  end
89
151
  end
90
152
  end