rack 1.6.11 → 2.2.0

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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +675 -0
  3. data/CONTRIBUTING.md +136 -0
  4. data/{COPYING → MIT-LICENSE} +4 -2
  5. data/README.rdoc +157 -163
  6. data/Rakefile +38 -32
  7. data/{SPEC → SPEC.rdoc} +41 -13
  8. data/bin/rackup +1 -0
  9. data/contrib/rack_logo.svg +164 -111
  10. data/example/lobster.ru +2 -0
  11. data/example/protectedlobster.rb +4 -2
  12. data/example/protectedlobster.ru +3 -1
  13. data/lib/rack/auth/abstract/handler.rb +3 -1
  14. data/lib/rack/auth/abstract/request.rb +6 -2
  15. data/lib/rack/auth/basic.rb +7 -4
  16. data/lib/rack/auth/digest/md5.rb +13 -11
  17. data/lib/rack/auth/digest/nonce.rb +6 -3
  18. data/lib/rack/auth/digest/params.rb +5 -4
  19. data/lib/rack/auth/digest/request.rb +6 -4
  20. data/lib/rack/body_proxy.rb +21 -15
  21. data/lib/rack/builder.rb +119 -26
  22. data/lib/rack/cascade.rb +28 -12
  23. data/lib/rack/chunked.rb +70 -22
  24. data/lib/rack/common_logger.rb +80 -0
  25. data/lib/rack/{conditionalget.rb → conditional_get.rb} +20 -16
  26. data/lib/rack/config.rb +2 -0
  27. data/lib/rack/content_length.rb +9 -8
  28. data/lib/rack/content_type.rb +5 -4
  29. data/lib/rack/core_ext/regexp.rb +14 -0
  30. data/lib/rack/deflater.rb +60 -70
  31. data/lib/rack/directory.rb +117 -85
  32. data/lib/rack/etag.rb +9 -7
  33. data/lib/rack/events.rb +153 -0
  34. data/lib/rack/file.rb +4 -149
  35. data/lib/rack/files.rb +218 -0
  36. data/lib/rack/handler/cgi.rb +17 -19
  37. data/lib/rack/handler/fastcgi.rb +17 -18
  38. data/lib/rack/handler/lsws.rb +14 -14
  39. data/lib/rack/handler/scgi.rb +22 -21
  40. data/lib/rack/handler/thin.rb +20 -11
  41. data/lib/rack/handler/webrick.rb +39 -32
  42. data/lib/rack/handler.rb +9 -26
  43. data/lib/rack/head.rb +16 -18
  44. data/lib/rack/lint.rb +110 -64
  45. data/lib/rack/lobster.rb +10 -10
  46. data/lib/rack/lock.rb +17 -11
  47. data/lib/rack/logger.rb +4 -2
  48. data/lib/rack/media_type.rb +43 -0
  49. data/lib/rack/{methodoverride.rb → method_override.rb} +10 -8
  50. data/lib/rack/mime.rb +27 -6
  51. data/lib/rack/mock.rb +124 -65
  52. data/lib/rack/multipart/generator.rb +20 -16
  53. data/lib/rack/multipart/parser.rb +273 -162
  54. data/lib/rack/multipart/uploaded_file.rb +15 -8
  55. data/lib/rack/multipart.rb +39 -8
  56. data/lib/rack/{nulllogger.rb → null_logger.rb} +3 -1
  57. data/lib/rack/query_parser.rb +217 -0
  58. data/lib/rack/recursive.rb +11 -9
  59. data/lib/rack/reloader.rb +8 -4
  60. data/lib/rack/request.rb +543 -305
  61. data/lib/rack/response.rb +244 -88
  62. data/lib/rack/rewindable_input.rb +5 -15
  63. data/lib/rack/runtime.rb +12 -18
  64. data/lib/rack/sendfile.rb +17 -15
  65. data/lib/rack/server.rb +125 -47
  66. data/lib/rack/session/abstract/id.rb +216 -93
  67. data/lib/rack/session/cookie.rb +47 -31
  68. data/lib/rack/session/memcache.rb +4 -87
  69. data/lib/rack/session/pool.rb +26 -17
  70. data/lib/rack/show_exceptions.rb +390 -0
  71. data/lib/rack/{showstatus.rb → show_status.rb} +8 -8
  72. data/lib/rack/static.rb +48 -11
  73. data/lib/rack/tempfile_reaper.rb +3 -3
  74. data/lib/rack/urlmap.rb +26 -19
  75. data/lib/rack/utils.rb +208 -294
  76. data/lib/rack/version.rb +29 -0
  77. data/lib/rack.rb +76 -33
  78. data/rack.gemspec +43 -30
  79. metadata +62 -183
  80. data/HISTORY.md +0 -375
  81. data/KNOWN-ISSUES +0 -44
  82. data/lib/rack/backports/uri/common_18.rb +0 -56
  83. data/lib/rack/backports/uri/common_192.rb +0 -52
  84. data/lib/rack/backports/uri/common_193.rb +0 -29
  85. data/lib/rack/commonlogger.rb +0 -72
  86. data/lib/rack/handler/evented_mongrel.rb +0 -8
  87. data/lib/rack/handler/mongrel.rb +0 -106
  88. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  89. data/lib/rack/showexceptions.rb +0 -387
  90. data/lib/rack/utils/okjson.rb +0 -600
  91. data/test/builder/anything.rb +0 -5
  92. data/test/builder/comment.ru +0 -4
  93. data/test/builder/end.ru +0 -5
  94. data/test/builder/line.ru +0 -1
  95. data/test/builder/options.ru +0 -2
  96. data/test/cgi/assets/folder/test.js +0 -1
  97. data/test/cgi/assets/fonts/font.eot +0 -1
  98. data/test/cgi/assets/images/image.png +0 -1
  99. data/test/cgi/assets/index.html +0 -1
  100. data/test/cgi/assets/javascripts/app.js +0 -1
  101. data/test/cgi/assets/stylesheets/app.css +0 -1
  102. data/test/cgi/lighttpd.conf +0 -26
  103. data/test/cgi/rackup_stub.rb +0 -6
  104. data/test/cgi/sample_rackup.ru +0 -5
  105. data/test/cgi/test +0 -9
  106. data/test/cgi/test+directory/test+file +0 -1
  107. data/test/cgi/test.fcgi +0 -8
  108. data/test/cgi/test.ru +0 -5
  109. data/test/gemloader.rb +0 -10
  110. data/test/multipart/bad_robots +0 -259
  111. data/test/multipart/binary +0 -0
  112. data/test/multipart/content_type_and_no_filename +0 -6
  113. data/test/multipart/empty +0 -10
  114. data/test/multipart/fail_16384_nofile +0 -814
  115. data/test/multipart/file1.txt +0 -1
  116. data/test/multipart/filename_and_modification_param +0 -7
  117. data/test/multipart/filename_and_no_name +0 -6
  118. data/test/multipart/filename_with_escaped_quotes +0 -6
  119. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  120. data/test/multipart/filename_with_null_byte +0 -7
  121. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  122. data/test/multipart/filename_with_unescaped_percentages +0 -6
  123. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  124. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  125. data/test/multipart/filename_with_unescaped_quotes +0 -6
  126. data/test/multipart/ie +0 -6
  127. data/test/multipart/invalid_character +0 -6
  128. data/test/multipart/mixed_files +0 -21
  129. data/test/multipart/nested +0 -10
  130. data/test/multipart/none +0 -9
  131. data/test/multipart/semicolon +0 -6
  132. data/test/multipart/text +0 -15
  133. data/test/multipart/three_files_three_fields +0 -31
  134. data/test/multipart/webkit +0 -32
  135. data/test/rackup/config.ru +0 -31
  136. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  137. data/test/spec_auth_basic.rb +0 -81
  138. data/test/spec_auth_digest.rb +0 -259
  139. data/test/spec_body_proxy.rb +0 -85
  140. data/test/spec_builder.rb +0 -223
  141. data/test/spec_cascade.rb +0 -61
  142. data/test/spec_cgi.rb +0 -102
  143. data/test/spec_chunked.rb +0 -101
  144. data/test/spec_commonlogger.rb +0 -93
  145. data/test/spec_conditionalget.rb +0 -102
  146. data/test/spec_config.rb +0 -22
  147. data/test/spec_content_length.rb +0 -85
  148. data/test/spec_content_type.rb +0 -45
  149. data/test/spec_deflater.rb +0 -339
  150. data/test/spec_directory.rb +0 -88
  151. data/test/spec_etag.rb +0 -107
  152. data/test/spec_fastcgi.rb +0 -107
  153. data/test/spec_file.rb +0 -221
  154. data/test/spec_handler.rb +0 -72
  155. data/test/spec_head.rb +0 -45
  156. data/test/spec_lint.rb +0 -550
  157. data/test/spec_lobster.rb +0 -58
  158. data/test/spec_lock.rb +0 -164
  159. data/test/spec_logger.rb +0 -23
  160. data/test/spec_methodoverride.rb +0 -111
  161. data/test/spec_mime.rb +0 -51
  162. data/test/spec_mock.rb +0 -297
  163. data/test/spec_mongrel.rb +0 -182
  164. data/test/spec_multipart.rb +0 -600
  165. data/test/spec_nulllogger.rb +0 -20
  166. data/test/spec_recursive.rb +0 -72
  167. data/test/spec_request.rb +0 -1232
  168. data/test/spec_response.rb +0 -407
  169. data/test/spec_rewindable_input.rb +0 -118
  170. data/test/spec_runtime.rb +0 -49
  171. data/test/spec_sendfile.rb +0 -130
  172. data/test/spec_server.rb +0 -167
  173. data/test/spec_session_abstract_id.rb +0 -53
  174. data/test/spec_session_cookie.rb +0 -410
  175. data/test/spec_session_memcache.rb +0 -321
  176. data/test/spec_session_pool.rb +0 -209
  177. data/test/spec_showexceptions.rb +0 -98
  178. data/test/spec_showstatus.rb +0 -103
  179. data/test/spec_static.rb +0 -145
  180. data/test/spec_tempfile_reaper.rb +0 -63
  181. data/test/spec_thin.rb +0 -91
  182. data/test/spec_urlmap.rb +0 -236
  183. data/test/spec_utils.rb +0 -647
  184. data/test/spec_version.rb +0 -17
  185. data/test/spec_webrick.rb +0 -184
  186. data/test/static/another/index.html +0 -1
  187. data/test/static/index.html +0 -1
  188. data/test/testrequest.rb +0 -78
  189. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  190. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,9 +1,11 @@
1
- require 'rack/utils'
2
- require 'rack/body_proxy'
1
+ # frozen_string_literal: true
3
2
 
4
3
  module Rack
5
4
 
6
- # Sets the Content-Length header on responses with fixed-length bodies.
5
+ # Sets the Content-Length header on responses that do not specify
6
+ # a Content-Length or Transfer-Encoding header. Note that this
7
+ # does not fix responses that have an invalid Content-Length
8
+ # header specified.
7
9
  class ContentLength
8
10
  include Rack::Utils
9
11
 
@@ -13,16 +15,15 @@ module Rack
13
15
 
14
16
  def call(env)
15
17
  status, headers, body = @app.call(env)
16
- headers = HeaderHash.new(headers)
18
+ headers = HeaderHash[headers]
17
19
 
18
- if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
20
+ if !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
19
21
  !headers[CONTENT_LENGTH] &&
20
- !headers['Transfer-Encoding'] &&
21
- body.respond_to?(:to_ary)
22
+ !headers[TRANSFER_ENCODING]
22
23
 
23
24
  obody = body
24
25
  body, length = [], 0
25
- obody.each { |part| body << part; length += bytesize(part) }
26
+ obody.each { |part| body << part; length += part.bytesize }
26
27
 
27
28
  body = BodyProxy.new(body) do
28
29
  obody.close if obody.respond_to?(:close)
@@ -1,4 +1,4 @@
1
- require 'rack/utils'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Rack
4
4
 
@@ -7,7 +7,8 @@ module Rack
7
7
  # Builder Usage:
8
8
  # use Rack::ContentType, "text/plain"
9
9
  #
10
- # When no content type argument is provided, "text/html" is assumed.
10
+ # When no content type argument is provided, "text/html" is the
11
+ # default.
11
12
  class ContentType
12
13
  include Rack::Utils
13
14
 
@@ -17,9 +18,9 @@ module Rack
17
18
 
18
19
  def call(env)
19
20
  status, headers, body = @app.call(env)
20
- headers = Utils::HeaderHash.new(headers)
21
+ headers = Utils::HeaderHash[headers]
21
22
 
22
- unless STATUS_WITH_NO_ENTITY_BODY.include?(status)
23
+ unless STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i)
23
24
  headers[CONTENT_TYPE] ||= @content_type
24
25
  end
25
26
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Regexp has `match?` since Ruby 2.4
4
+ # so to support Ruby < 2.4 we need to define this method
5
+
6
+ module Rack
7
+ module RegexpExtensions
8
+ refine Regexp do
9
+ def match?(string, pos = 0)
10
+ !!match(string, pos)
11
+ end
12
+ end unless //.respond_to?(:match?)
13
+ end
14
+ end
data/lib/rack/deflater.rb CHANGED
@@ -1,39 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "zlib"
2
4
  require "time" # for Time.httpdate
3
- require 'rack/utils'
4
5
 
5
6
  module Rack
6
- # This middleware enables compression of http responses.
7
+ # This middleware enables content encoding of http responses,
8
+ # usually for purposes of compression.
9
+ #
10
+ # Currently supported encodings:
7
11
  #
8
- # Currently supported compression algorithms:
12
+ # * gzip
13
+ # * identity (no transformation)
9
14
  #
10
- # * gzip
11
- # * deflate
12
- # * identity (no transformation)
15
+ # This middleware automatically detects when encoding is supported
16
+ # and allowed. For example no encoding is made when a cache
17
+ # directive of 'no-transform' is present, when the response status
18
+ # code is one that doesn't allow an entity body, or when the body
19
+ # is empty.
13
20
  #
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.
21
+ # Note that despite the name, Deflater does not support the +deflate+
22
+ # encoding.
18
23
  class Deflater
19
- ##
20
- # Creates Rack::Deflater middleware.
24
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
25
+
26
+ # Creates Rack::Deflater middleware. Options:
21
27
  #
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
28
+ # :if :: a lambda enabling / disabling deflation based on returned boolean value
29
+ # (e.g <tt>use Rack::Deflater, :if => lambda { |*, body| sum=0; body.each { |i| sum += i.length }; sum > 512 }</tt>).
30
+ # However, be aware that calling `body.each` inside the block will break cases where `body.each` is not idempotent,
31
+ # such as when it is an +IO+ instance.
32
+ # :include :: a list of content types that should be compressed. By default, all content types are compressed.
33
+ # :sync :: determines if the stream is going to be flushed after every chunk. Flushing after every chunk reduces
34
+ # latency for time-sensitive streaming applications, but hurts compression and throughput.
35
+ # Defaults to +true+.
27
36
  def initialize(app, options = {})
28
37
  @app = app
29
-
30
38
  @condition = options[:if]
31
39
  @compressible_types = options[:include]
40
+ @sync = options.fetch(:sync, true)
32
41
  end
33
42
 
34
43
  def call(env)
35
44
  status, headers, body = @app.call(env)
36
- headers = Utils::HeaderHash.new(headers)
45
+ headers = Utils::HeaderHash[headers]
37
46
 
38
47
  unless should_deflate?(env, status, headers, body)
39
48
  return [status, headers, body]
@@ -41,11 +50,11 @@ module Rack
41
50
 
42
51
  request = Request.new(env)
43
52
 
44
- encoding = Utils.select_best_encoding(%w(gzip deflate identity),
53
+ encoding = Utils.select_best_encoding(%w(gzip identity),
45
54
  request.accept_encoding)
46
55
 
47
56
  # Set the Vary HTTP header.
48
- vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
57
+ vary = headers["Vary"].to_s.split(",").map(&:strip)
49
58
  unless vary.include?("*") || vary.include?("Accept-Encoding")
50
59
  headers["Vary"] = vary.push("Accept-Encoding").join(",")
51
60
  end
@@ -54,91 +63,68 @@ module Rack
54
63
  when "gzip"
55
64
  headers['Content-Encoding'] = "gzip"
56
65
  headers.delete(CONTENT_LENGTH)
57
- mtime = headers.key?("Last-Modified") ?
58
- Time.httpdate(headers["Last-Modified"]) : Time.now
59
- [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)]
66
+ mtime = headers["Last-Modified"]
67
+ mtime = Time.httpdate(mtime).to_i if mtime
68
+ [status, headers, GzipStream.new(body, mtime, @sync)]
64
69
  when "identity"
65
70
  [status, headers, body]
66
71
  when nil
67
72
  message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
68
73
  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]
74
+ [406, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s }, bp]
70
75
  end
71
76
  end
72
77
 
78
+ # Body class used for gzip encoded responses.
73
79
  class GzipStream
74
- def initialize(body, mtime)
80
+ # Initialize the gzip stream. Arguments:
81
+ # body :: Response body to compress with gzip
82
+ # mtime :: The modification time of the body, used to set the
83
+ # modification time in the gzip header.
84
+ # sync :: Whether to flush each gzip chunk as soon as it is ready.
85
+ def initialize(body, mtime, sync)
75
86
  @body = body
76
87
  @mtime = mtime
77
- @closed = false
88
+ @sync = sync
78
89
  end
79
90
 
91
+ # Yield gzip compressed strings to the given block.
80
92
  def each(&block)
81
93
  @writer = block
82
- gzip =::Zlib::GzipWriter.new(self)
83
- gzip.mtime = @mtime
94
+ gzip = ::Zlib::GzipWriter.new(self)
95
+ gzip.mtime = @mtime if @mtime
84
96
  @body.each { |part|
97
+ # Skip empty strings, as they would result in no output,
98
+ # and flushing empty parts would raise Zlib::BufError.
99
+ next if part.empty?
100
+
85
101
  gzip.write(part)
86
- gzip.flush
102
+ gzip.flush if @sync
87
103
  }
88
104
  ensure
89
105
  gzip.close
90
- @writer = nil
91
106
  end
92
107
 
108
+ # Call the block passed to #each with the the gzipped data.
93
109
  def write(data)
94
110
  @writer.call(data)
95
111
  end
96
112
 
113
+ # Close the original body if possible.
97
114
  def close
98
- return if @closed
99
- @closed = true
100
- @body.close if @body.respond_to?(:close)
101
- end
102
- end
103
-
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
115
  @body.close if @body.respond_to?(:close)
131
116
  end
132
117
  end
133
118
 
134
119
  private
135
120
 
121
+ # Whether the body should be compressed.
136
122
  def should_deflate?(env, status, headers, body)
137
123
  # Skip compressing empty entity body responses and responses with
138
124
  # 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/)
125
+ if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
126
+ /\bno-transform\b/.match?(headers['Cache-Control'].to_s) ||
127
+ headers['Content-Encoding']&.!~(/\bidentity\b/)
142
128
  return false
143
129
  end
144
130
 
@@ -148,6 +134,10 @@ module Rack
148
134
  # Skip if @condition lambda is given and evaluates to false
149
135
  return false if @condition && !@condition.call(env, status, headers, body)
150
136
 
137
+ # No point in compressing empty body, also handles usage with
138
+ # Rack::Sendfile.
139
+ return false if headers[CONTENT_LENGTH] == '0'
140
+
151
141
  true
152
142
  end
153
143
  end
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'time'
2
- require 'rack/utils'
3
- require 'rack/mime'
4
4
 
5
5
  module Rack
6
6
  # Rack::Directory serves entries below the +root+ given, according to the
@@ -8,11 +8,11 @@ module Rack
8
8
  # will be presented in an html based index. If a file is found, the env will
9
9
  # be passed to the specified +app+.
10
10
  #
11
- # If +app+ is not specified, a Rack::File of the same +root+ will be used.
11
+ # If +app+ is not specified, a Rack::Files of the same +root+ will be used.
12
12
 
13
13
  class Directory
14
- DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>"
15
- DIR_PAGE = <<-PAGE
14
+ DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
15
+ DIR_PAGE_HEADER = <<-PAGE
16
16
  <html><head>
17
17
  <title>%s</title>
18
18
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
@@ -33,116 +33,153 @@ table { width:100%%; }
33
33
  <th class='type'>Type</th>
34
34
  <th class='mtime'>Last Modified</th>
35
35
  </tr>
36
- %s
36
+ PAGE
37
+ DIR_PAGE_FOOTER = <<-PAGE
37
38
  </table>
38
39
  <hr />
39
40
  </body></html>
40
41
  PAGE
41
42
 
42
- attr_reader :files
43
- attr_accessor :root, :path
43
+ # Body class for directory entries, showing an index page with links
44
+ # to each file.
45
+ class DirectoryBody < Struct.new(:root, :path, :files)
46
+ # Yield strings for each part of the directory entry
47
+ def each
48
+ show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
49
+ yield(DIR_PAGE_HEADER % [ show_path, show_path ])
50
+
51
+ unless path.chomp('/') == root
52
+ yield(DIR_FILE % DIR_FILE_escape(files.call('..')))
53
+ end
54
+
55
+ Dir.foreach(path) do |basename|
56
+ next if basename.start_with?('.')
57
+ next unless f = files.call(basename)
58
+ yield(DIR_FILE % DIR_FILE_escape(f))
59
+ end
60
+
61
+ yield(DIR_PAGE_FOOTER)
62
+ end
63
+
64
+ private
44
65
 
45
- def initialize(root, app=nil)
46
- @root = F.expand_path(root)
47
- @app = app || Rack::File.new(@root)
66
+ # Escape each element in the array of html strings.
67
+ def DIR_FILE_escape(htmls)
68
+ htmls.map { |e| Utils.escape_html(e) }
69
+ end
48
70
  end
49
71
 
50
- def call(env)
51
- dup._call(env)
72
+ # The root of the directory hierarchy. Only requests for files and
73
+ # directories inside of the root directory are supported.
74
+ attr_reader :root
75
+
76
+ # Set the root directory and application for serving files.
77
+ def initialize(root, app = nil)
78
+ @root = ::File.expand_path(root)
79
+ @app = app || Files.new(@root)
80
+ @head = Head.new(method(:get))
52
81
  end
53
82
 
54
- F = ::File
83
+ def call(env)
84
+ # strip body if this is a HEAD call
85
+ @head.call env
86
+ end
55
87
 
56
- def _call(env)
57
- @env = env
58
- @script_name = env[SCRIPT_NAME]
59
- @path_info = Utils.unescape(env[PATH_INFO])
88
+ # Internals of request handling. Similar to call but does
89
+ # not remove body for HEAD requests.
90
+ def get(env)
91
+ script_name = env[SCRIPT_NAME]
92
+ path_info = Utils.unescape_path(env[PATH_INFO])
60
93
 
61
- if forbidden = check_forbidden
62
- forbidden
94
+ if client_error_response = check_bad_request(path_info) || check_forbidden(path_info)
95
+ client_error_response
63
96
  else
64
- @path = F.join(@root, @path_info)
65
- list_path
97
+ path = ::File.join(@root, path_info)
98
+ list_path(env, path, path_info, script_name)
66
99
  end
67
100
  end
68
101
 
69
- def check_forbidden
70
- return unless @path_info.include? ".."
102
+ # Rack response to use for requests with invalid paths, or nil if path is valid.
103
+ def check_bad_request(path_info)
104
+ return if Utils.valid_path?(path_info)
71
105
 
72
- body = "Forbidden\n"
73
- size = Rack::Utils.bytesize(body)
74
- return [403, {"Content-Type" => "text/plain",
75
- CONTENT_LENGTH => size.to_s,
76
- "X-Cascade" => "pass"}, [body]]
106
+ body = "Bad Request\n"
107
+ [400, { CONTENT_TYPE => "text/plain",
108
+ CONTENT_LENGTH => body.bytesize.to_s,
109
+ "X-Cascade" => "pass" }, [body]]
77
110
  end
78
111
 
79
- def list_directory
80
- @files = [['../','Parent Directory','','','']]
81
- glob = F.join(@path, '*')
112
+ # Rack response to use for requests with paths outside the root, or nil if path is inside the root.
113
+ def check_forbidden(path_info)
114
+ return unless path_info.include? ".."
115
+ return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
116
+
117
+ body = "Forbidden\n"
118
+ [403, { CONTENT_TYPE => "text/plain",
119
+ CONTENT_LENGTH => body.bytesize.to_s,
120
+ "X-Cascade" => "pass" }, [body]]
121
+ end
82
122
 
83
- url_head = (@script_name.split('/') + @path_info.split('/')).map do |part|
84
- Rack::Utils.escape part
123
+ # Rack response to use for directories under the root.
124
+ def list_directory(path_info, path, script_name)
125
+ url_head = (script_name.split('/') + path_info.split('/')).map do |part|
126
+ Utils.escape_path part
85
127
  end
86
128
 
87
- Dir[glob].sort.each do |node|
88
- stat = stat(node)
89
- next unless stat
90
- basename = F.basename(node)
91
- ext = F.extname(node)
129
+ # Globbing not safe as path could contain glob metacharacters
130
+ body = DirectoryBody.new(@root, path, ->(basename) do
131
+ stat = stat(::File.join(path, basename))
132
+ next unless stat
92
133
 
93
- url = F.join(*url_head + [Rack::Utils.escape(basename)])
94
- size = stat.size
95
- type = stat.directory? ? 'directory' : Mime.mime_type(ext)
96
- size = stat.directory? ? '-' : filesize_format(size)
134
+ url = ::File.join(*url_head + [Utils.escape_path(basename)])
97
135
  mtime = stat.mtime.httpdate
98
- url << '/' if stat.directory?
99
- basename << '/' if stat.directory?
100
-
101
- @files << [ url, basename, size, type, mtime ]
102
- end
103
-
104
- return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, self ]
136
+ if stat.directory?
137
+ type = 'directory'
138
+ size = '-'
139
+ url << '/'
140
+ if basename == '..'
141
+ basename = 'Parent Directory'
142
+ else
143
+ basename << '/'
144
+ end
145
+ else
146
+ type = Mime.mime_type(::File.extname(basename))
147
+ size = filesize_format(stat.size)
148
+ end
149
+
150
+ [ url, basename, size, type, mtime ]
151
+ end)
152
+
153
+ [ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, body ]
105
154
  end
106
155
 
107
- def stat(node, max = 10)
108
- F.stat(node)
156
+ # File::Stat for the given path, but return nil for missing/bad entries.
157
+ def stat(path)
158
+ ::File.stat(path)
109
159
  rescue Errno::ENOENT, Errno::ELOOP
110
160
  return nil
111
161
  end
112
162
 
113
- # TODO: add correct response if not readable, not sure if 404 is the best
114
- # option
115
- def list_path
116
- @stat = F.stat(@path)
117
-
118
- if @stat.readable?
119
- return @app.call(@env) if @stat.file?
120
- return list_directory if @stat.directory?
121
- else
122
- raise Errno::ENOENT, 'No such file or directory'
163
+ # Rack response to use for files and directories under the root.
164
+ # Unreadable and non-file, non-directory entries will get a 404 response.
165
+ def list_path(env, path, path_info, script_name)
166
+ if (stat = stat(path)) && stat.readable?
167
+ return @app.call(env) if stat.file?
168
+ return list_directory(path_info, path, script_name) if stat.directory?
123
169
  end
124
170
 
125
- rescue Errno::ENOENT, Errno::ELOOP
126
- return entity_not_found
127
- end
128
-
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",
133
- CONTENT_LENGTH => size.to_s,
134
- "X-Cascade" => "pass"}, [body]]
171
+ entity_not_found(path_info)
135
172
  end
136
173
 
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 }
174
+ # Rack response to use for unreadable and non-file, non-directory entries.
175
+ def entity_not_found(path_info)
176
+ body = "Entity not found: #{path_info}\n"
177
+ [404, { CONTENT_TYPE => "text/plain",
178
+ CONTENT_LENGTH => body.bytesize.to_s,
179
+ "X-Cascade" => "pass" }, [body]]
142
180
  end
143
181
 
144
182
  # Stolen from Ramaze
145
-
146
183
  FILESIZE_FORMAT = [
147
184
  ['%.1fT', 1 << 40],
148
185
  ['%.1fG', 1 << 30],
@@ -150,18 +187,13 @@ table { width:100%%; }
150
187
  ['%.1fK', 1 << 10],
151
188
  ]
152
189
 
190
+ # Provide human readable file sizes
153
191
  def filesize_format(int)
154
192
  FILESIZE_FORMAT.each do |format, size|
155
193
  return format % (int.to_f / size) if int >= size
156
194
  end
157
195
 
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) }]
196
+ "#{int}B"
165
197
  end
166
198
  end
167
199
  end
data/lib/rack/etag.rb CHANGED
@@ -1,4 +1,7 @@
1
- require 'digest/md5'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../rack'
4
+ require 'digest/sha2'
2
5
 
3
6
  module Rack
4
7
  # Automatically sets the ETag header on all String bodies.
@@ -11,8 +14,8 @@ module Rack
11
14
  # used when Etag is absent and a directive when it is present. The first
12
15
  # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
13
16
  class ETag
14
- ETAG_STRING = 'ETag'.freeze
15
- DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
17
+ ETAG_STRING = Rack::ETAG
18
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
16
19
 
17
20
  def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
18
21
  @app = app
@@ -54,8 +57,7 @@ module Rack
54
57
  end
55
58
 
56
59
  def skip_caching?(headers)
57
- (headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
58
- headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
60
+ headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
59
61
  end
60
62
 
61
63
  def digest_body(body)
@@ -64,10 +66,10 @@ module Rack
64
66
 
65
67
  body.each do |part|
66
68
  parts << part
67
- (digest ||= Digest::MD5.new) << part unless part.empty?
69
+ (digest ||= Digest::SHA256.new) << part unless part.empty?
68
70
  end
69
71
 
70
- [digest && digest.hexdigest, parts]
72
+ [digest && digest.hexdigest.byteslice(0, 32), parts]
71
73
  end
72
74
  end
73
75
  end