rack 2.0.9.3 → 3.0.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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +808 -0
  3. data/CONTRIBUTING.md +142 -0
  4. data/{COPYING → MIT-LICENSE} +4 -2
  5. data/README.md +293 -0
  6. data/SPEC.rdoc +340 -0
  7. data/lib/rack/auth/abstract/handler.rb +6 -2
  8. data/lib/rack/auth/abstract/request.rb +4 -2
  9. data/lib/rack/auth/basic.rb +7 -4
  10. data/lib/rack/auth/digest/md5.rb +1 -129
  11. data/lib/rack/auth/digest/nonce.rb +1 -51
  12. data/lib/rack/auth/digest/params.rb +1 -52
  13. data/lib/rack/auth/digest/request.rb +1 -41
  14. data/lib/rack/auth/digest.rb +256 -0
  15. data/lib/rack/body_proxy.rb +18 -15
  16. data/lib/rack/builder.rb +151 -40
  17. data/lib/rack/cascade.rb +30 -12
  18. data/lib/rack/chunked.rb +74 -23
  19. data/lib/rack/common_logger.rb +49 -36
  20. data/lib/rack/conditional_get.rb +33 -26
  21. data/lib/rack/config.rb +2 -0
  22. data/lib/rack/constants.rb +63 -0
  23. data/lib/rack/content_length.rb +13 -16
  24. data/lib/rack/content_type.rb +12 -8
  25. data/lib/rack/deflater.rb +84 -45
  26. data/lib/rack/directory.rb +90 -64
  27. data/lib/rack/etag.rb +17 -23
  28. data/lib/rack/events.rb +23 -20
  29. data/lib/rack/file.rb +5 -172
  30. data/lib/rack/files.rb +216 -0
  31. data/lib/rack/head.rb +10 -9
  32. data/lib/rack/headers.rb +154 -0
  33. data/lib/rack/lint.rb +786 -645
  34. data/lib/rack/lock.rb +4 -6
  35. data/lib/rack/logger.rb +4 -0
  36. data/lib/rack/media_type.rb +10 -5
  37. data/lib/rack/method_override.rb +8 -2
  38. data/lib/rack/mime.rb +17 -1
  39. data/lib/rack/mock.rb +2 -195
  40. data/lib/rack/mock_request.rb +166 -0
  41. data/lib/rack/mock_response.rb +126 -0
  42. data/lib/rack/multipart/generator.rb +21 -15
  43. data/lib/rack/multipart/parser.rb +161 -118
  44. data/lib/rack/multipart/uploaded_file.rb +19 -7
  45. data/lib/rack/multipart.rb +23 -41
  46. data/lib/rack/null_logger.rb +11 -0
  47. data/lib/rack/query_parser.rb +126 -65
  48. data/lib/rack/recursive.rb +9 -5
  49. data/lib/rack/reloader.rb +6 -4
  50. data/lib/rack/request.rb +331 -74
  51. data/lib/rack/response.rb +223 -70
  52. data/lib/rack/rewindable_input.rb +28 -8
  53. data/lib/rack/runtime.rb +11 -8
  54. data/lib/rack/sendfile.rb +42 -33
  55. data/lib/rack/show_exceptions.rb +35 -18
  56. data/lib/rack/show_status.rb +25 -15
  57. data/lib/rack/static.rb +30 -18
  58. data/lib/rack/tempfile_reaper.rb +16 -5
  59. data/lib/rack/urlmap.rb +14 -6
  60. data/lib/rack/utils.rb +268 -260
  61. data/lib/rack/version.rb +34 -0
  62. data/lib/rack.rb +15 -92
  63. metadata +44 -207
  64. data/HISTORY.md +0 -520
  65. data/README.rdoc +0 -316
  66. data/Rakefile +0 -116
  67. data/SPEC +0 -263
  68. data/bin/rackup +0 -4
  69. data/contrib/rack.png +0 -0
  70. data/contrib/rack.svg +0 -150
  71. data/contrib/rack_logo.svg +0 -164
  72. data/contrib/rdoc.css +0 -412
  73. data/example/lobster.ru +0 -4
  74. data/example/protectedlobster.rb +0 -14
  75. data/example/protectedlobster.ru +0 -8
  76. data/lib/rack/handler/cgi.rb +0 -60
  77. data/lib/rack/handler/fastcgi.rb +0 -100
  78. data/lib/rack/handler/lsws.rb +0 -61
  79. data/lib/rack/handler/scgi.rb +0 -70
  80. data/lib/rack/handler/thin.rb +0 -36
  81. data/lib/rack/handler/webrick.rb +0 -120
  82. data/lib/rack/handler.rb +0 -99
  83. data/lib/rack/lobster.rb +0 -70
  84. data/lib/rack/server.rb +0 -395
  85. data/lib/rack/session/abstract/id.rb +0 -510
  86. data/lib/rack/session/cookie.rb +0 -204
  87. data/lib/rack/session/memcache.rb +0 -99
  88. data/lib/rack/session/pool.rb +0 -83
  89. data/rack.gemspec +0 -34
  90. data/test/builder/an_underscore_app.rb +0 -5
  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 -9
  108. data/test/cgi/test.gz +0 -0
  109. data/test/cgi/test.ru +0 -5
  110. data/test/gemloader.rb +0 -10
  111. data/test/helper.rb +0 -34
  112. data/test/multipart/bad_robots +0 -259
  113. data/test/multipart/binary +0 -0
  114. data/test/multipart/content_type_and_no_filename +0 -6
  115. data/test/multipart/empty +0 -10
  116. data/test/multipart/fail_16384_nofile +0 -814
  117. data/test/multipart/file1.txt +0 -1
  118. data/test/multipart/filename_and_modification_param +0 -7
  119. data/test/multipart/filename_and_no_name +0 -6
  120. data/test/multipart/filename_with_encoded_words +0 -7
  121. data/test/multipart/filename_with_escaped_quotes +0 -6
  122. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  123. data/test/multipart/filename_with_null_byte +0 -7
  124. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  125. data/test/multipart/filename_with_single_quote +0 -7
  126. data/test/multipart/filename_with_unescaped_percentages +0 -6
  127. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  128. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  129. data/test/multipart/filename_with_unescaped_quotes +0 -6
  130. data/test/multipart/ie +0 -6
  131. data/test/multipart/invalid_character +0 -6
  132. data/test/multipart/mixed_files +0 -21
  133. data/test/multipart/nested +0 -10
  134. data/test/multipart/none +0 -9
  135. data/test/multipart/quoted +0 -15
  136. data/test/multipart/rack-logo.png +0 -0
  137. data/test/multipart/semicolon +0 -6
  138. data/test/multipart/text +0 -15
  139. data/test/multipart/three_files_three_fields +0 -31
  140. data/test/multipart/unity3d_wwwform +0 -11
  141. data/test/multipart/webkit +0 -32
  142. data/test/rackup/config.ru +0 -31
  143. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  144. data/test/spec_auth_basic.rb +0 -89
  145. data/test/spec_auth_digest.rb +0 -260
  146. data/test/spec_body_proxy.rb +0 -85
  147. data/test/spec_builder.rb +0 -233
  148. data/test/spec_cascade.rb +0 -63
  149. data/test/spec_cgi.rb +0 -84
  150. data/test/spec_chunked.rb +0 -103
  151. data/test/spec_common_logger.rb +0 -107
  152. data/test/spec_conditional_get.rb +0 -103
  153. data/test/spec_config.rb +0 -23
  154. data/test/spec_content_length.rb +0 -86
  155. data/test/spec_content_type.rb +0 -46
  156. data/test/spec_deflater.rb +0 -375
  157. data/test/spec_directory.rb +0 -148
  158. data/test/spec_etag.rb +0 -108
  159. data/test/spec_events.rb +0 -133
  160. data/test/spec_fastcgi.rb +0 -85
  161. data/test/spec_file.rb +0 -264
  162. data/test/spec_handler.rb +0 -57
  163. data/test/spec_head.rb +0 -46
  164. data/test/spec_lint.rb +0 -520
  165. data/test/spec_lobster.rb +0 -59
  166. data/test/spec_lock.rb +0 -204
  167. data/test/spec_logger.rb +0 -24
  168. data/test/spec_media_type.rb +0 -42
  169. data/test/spec_method_override.rb +0 -110
  170. data/test/spec_mime.rb +0 -51
  171. data/test/spec_mock.rb +0 -359
  172. data/test/spec_multipart.rb +0 -721
  173. data/test/spec_null_logger.rb +0 -21
  174. data/test/spec_recursive.rb +0 -75
  175. data/test/spec_request.rb +0 -1423
  176. data/test/spec_response.rb +0 -528
  177. data/test/spec_rewindable_input.rb +0 -128
  178. data/test/spec_runtime.rb +0 -50
  179. data/test/spec_sendfile.rb +0 -125
  180. data/test/spec_server.rb +0 -193
  181. data/test/spec_session_abstract_id.rb +0 -31
  182. data/test/spec_session_abstract_session_hash.rb +0 -45
  183. data/test/spec_session_cookie.rb +0 -442
  184. data/test/spec_session_memcache.rb +0 -357
  185. data/test/spec_session_persisted_secure_secure_session_hash.rb +0 -73
  186. data/test/spec_session_pool.rb +0 -247
  187. data/test/spec_show_exceptions.rb +0 -93
  188. data/test/spec_show_status.rb +0 -104
  189. data/test/spec_static.rb +0 -184
  190. data/test/spec_tempfile_reaper.rb +0 -64
  191. data/test/spec_thin.rb +0 -96
  192. data/test/spec_urlmap.rb +0 -237
  193. data/test/spec_utils.rb +0 -742
  194. data/test/spec_version.rb +0 -11
  195. data/test/spec_webrick.rb +0 -206
  196. data/test/static/another/index.html +0 -1
  197. data/test/static/foo.html +0 -1
  198. data/test/static/index.html +0 -1
  199. data/test/testrequest.rb +0 -78
  200. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  201. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/deflater.rb CHANGED
@@ -1,41 +1,53 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "zlib"
2
4
  require "time" # for Time.httpdate
3
- require 'rack/utils'
5
+
6
+ require_relative 'constants'
7
+ require_relative 'utils'
8
+ require_relative 'request'
9
+ require_relative 'body_proxy'
4
10
 
5
11
  module Rack
6
- # This middleware enables compression of http responses.
12
+ # This middleware enables content encoding of http responses,
13
+ # usually for purposes of compression.
7
14
  #
8
- # Currently supported compression algorithms:
15
+ # Currently supported encodings:
9
16
  #
10
- # * gzip
11
- # * identity (no transformation)
17
+ # * gzip
18
+ # * identity (no transformation)
12
19
  #
13
- # The middleware automatically detects when compression is supported
14
- # and allowed. For example no transformation is made when a cache
15
- # directive of 'no-transform' is present, or when the response status
16
- # code is one that doesn't allow an entity body.
20
+ # This middleware automatically detects when encoding is supported
21
+ # and allowed. For example no encoding is made when a cache
22
+ # directive of 'no-transform' is present, when the response status
23
+ # code is one that doesn't allow an entity body, or when the body
24
+ # is empty.
25
+ #
26
+ # Note that despite the name, Deflater does not support the +deflate+
27
+ # encoding.
17
28
  class Deflater
18
- ##
19
- # Creates Rack::Deflater middleware.
29
+ # Creates Rack::Deflater middleware. Options:
20
30
  #
21
- # [app] rack app instance
22
- # [options] hash of deflater options, i.e.
23
- # 'if' - a lambda enabling / disabling deflation based on returned boolean value
24
- # e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.map(&:bytesize).reduce(0, :+) > 512 }
25
- # 'include' - a list of content types that should be compressed
31
+ # :if :: a lambda enabling / disabling deflation based on returned boolean value
32
+ # (e.g <tt>use Rack::Deflater, :if => lambda { |*, body| sum=0; body.each { |i| sum += i.length }; sum > 512 }</tt>).
33
+ # However, be aware that calling `body.each` inside the block will break cases where `body.each` is not idempotent,
34
+ # such as when it is an +IO+ instance.
35
+ # :include :: a list of content types that should be compressed. By default, all content types are compressed.
36
+ # :sync :: determines if the stream is going to be flushed after every chunk. Flushing after every chunk reduces
37
+ # latency for time-sensitive streaming applications, but hurts compression and throughput.
38
+ # Defaults to +true+.
26
39
  def initialize(app, options = {})
27
40
  @app = app
28
-
29
41
  @condition = options[:if]
30
42
  @compressible_types = options[:include]
43
+ @sync = options.fetch(:sync, true)
31
44
  end
32
45
 
33
46
  def call(env)
34
- status, headers, body = @app.call(env)
35
- headers = Utils::HeaderHash.new(headers)
47
+ status, headers, body = response = @app.call(env)
36
48
 
37
49
  unless should_deflate?(env, status, headers, body)
38
- return [status, headers, body]
50
+ return response
39
51
  end
40
52
 
41
53
  request = Request.new(env)
@@ -44,66 +56,89 @@ module Rack
44
56
  request.accept_encoding)
45
57
 
46
58
  # Set the Vary HTTP header.
47
- vary = headers["Vary"].to_s.split(",").map(&:strip)
48
- unless vary.include?("*") || vary.include?("Accept-Encoding")
49
- headers["Vary"] = vary.push("Accept-Encoding").join(",")
59
+ vary = headers["vary"].to_s.split(",").map(&:strip)
60
+ unless vary.include?("*") || vary.any?{|v| v.downcase == 'accept-encoding'}
61
+ headers["vary"] = vary.push("Accept-Encoding").join(",")
50
62
  end
51
63
 
52
64
  case encoding
53
65
  when "gzip"
54
- headers['Content-Encoding'] = "gzip"
66
+ headers['content-encoding'] = "gzip"
55
67
  headers.delete(CONTENT_LENGTH)
56
- mtime = headers.key?("Last-Modified") ?
57
- Time.httpdate(headers["Last-Modified"]) : Time.now
58
- [status, headers, GzipStream.new(body, mtime)]
68
+ mtime = headers["last-modified"]
69
+ mtime = Time.httpdate(mtime).to_i if mtime
70
+ response[2] = GzipStream.new(body, mtime, @sync)
71
+ response
59
72
  when "identity"
60
- [status, headers, body]
61
- when nil
73
+ response
74
+ else # when nil
75
+ # Only possible encoding values here are 'gzip', 'identity', and nil
62
76
  message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
63
77
  bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
64
- [406, {CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s}, bp]
78
+ [406, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s }, bp]
65
79
  end
66
80
  end
67
81
 
82
+ # Body class used for gzip encoded responses.
68
83
  class GzipStream
69
- def initialize(body, mtime)
84
+
85
+ BUFFER_LENGTH = 128 * 1_024
86
+
87
+ # Initialize the gzip stream. Arguments:
88
+ # body :: Response body to compress with gzip
89
+ # mtime :: The modification time of the body, used to set the
90
+ # modification time in the gzip header.
91
+ # sync :: Whether to flush each gzip chunk as soon as it is ready.
92
+ def initialize(body, mtime, sync)
70
93
  @body = body
71
94
  @mtime = mtime
72
- @closed = false
95
+ @sync = sync
73
96
  end
74
97
 
98
+ # Yield gzip compressed strings to the given block.
75
99
  def each(&block)
76
100
  @writer = block
77
- gzip =::Zlib::GzipWriter.new(self)
78
- gzip.mtime = @mtime
79
- @body.each { |part|
80
- gzip.write(part)
81
- gzip.flush
82
- }
101
+ gzip = ::Zlib::GzipWriter.new(self)
102
+ gzip.mtime = @mtime if @mtime
103
+ # @body.each is equivalent to @body.gets (slow)
104
+ if @body.is_a? ::File # XXX: Should probably be ::IO
105
+ while part = @body.read(BUFFER_LENGTH)
106
+ gzip.write(part)
107
+ gzip.flush if @sync
108
+ end
109
+ else
110
+ @body.each { |part|
111
+ # Skip empty strings, as they would result in no output,
112
+ # and flushing empty parts would raise Zlib::BufError.
113
+ next if part.empty?
114
+ gzip.write(part)
115
+ gzip.flush if @sync
116
+ }
117
+ end
83
118
  ensure
84
- gzip.close
85
- @writer = nil
119
+ gzip.finish
86
120
  end
87
121
 
122
+ # Call the block passed to #each with the gzipped data.
88
123
  def write(data)
89
124
  @writer.call(data)
90
125
  end
91
126
 
127
+ # Close the original body if possible.
92
128
  def close
93
- return if @closed
94
- @closed = true
95
129
  @body.close if @body.respond_to?(:close)
96
130
  end
97
131
  end
98
132
 
99
133
  private
100
134
 
135
+ # Whether the body should be compressed.
101
136
  def should_deflate?(env, status, headers, body)
102
137
  # Skip compressing empty entity body responses and responses with
103
138
  # no-transform set.
104
- if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
105
- headers[CACHE_CONTROL].to_s =~ /\bno-transform\b/ ||
106
- (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
139
+ if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
140
+ /\bno-transform\b/.match?(headers[CACHE_CONTROL].to_s) ||
141
+ headers['content-encoding']&.!~(/\bidentity\b/)
107
142
  return false
108
143
  end
109
144
 
@@ -113,6 +148,10 @@ module Rack
113
148
  # Skip if @condition lambda is given and evaluates to false
114
149
  return false if @condition && !@condition.call(env, status, headers, body)
115
150
 
151
+ # No point in compressing empty body, also handles usage with
152
+ # Rack::Sendfile.
153
+ return false if headers[CONTENT_LENGTH] == '0'
154
+
116
155
  true
117
156
  end
118
157
  end
@@ -1,6 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'time'
2
- require 'rack/utils'
3
- require 'rack/mime'
4
+
5
+ require_relative 'constants'
6
+ require_relative 'utils'
7
+ require_relative 'head'
8
+ require_relative 'mime'
9
+ require_relative 'files'
4
10
 
5
11
  module Rack
6
12
  # Rack::Directory serves entries below the +root+ given, according to the
@@ -8,11 +14,11 @@ module Rack
8
14
  # will be presented in an html based index. If a file is found, the env will
9
15
  # be passed to the specified +app+.
10
16
  #
11
- # If +app+ is not specified, a Rack::File of the same +root+ will be used.
17
+ # If +app+ is not specified, a Rack::Files of the same +root+ will be used.
12
18
 
13
19
  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
20
+ 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"
21
+ DIR_PAGE_HEADER = <<-PAGE
16
22
  <html><head>
17
23
  <title>%s</title>
18
24
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
@@ -33,33 +39,51 @@ table { width:100%%; }
33
39
  <th class='type'>Type</th>
34
40
  <th class='mtime'>Last Modified</th>
35
41
  </tr>
36
- %s
42
+ PAGE
43
+ DIR_PAGE_FOOTER = <<-PAGE
37
44
  </table>
38
45
  <hr />
39
46
  </body></html>
40
47
  PAGE
41
48
 
49
+ # Body class for directory entries, showing an index page with links
50
+ # to each file.
42
51
  class DirectoryBody < Struct.new(:root, :path, :files)
52
+ # Yield strings for each part of the directory entry
43
53
  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 }
54
+ show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
55
+ yield(DIR_PAGE_HEADER % [ show_path, show_path ])
56
+
57
+ unless path.chomp('/') == root
58
+ yield(DIR_FILE % DIR_FILE_escape(files.call('..')))
59
+ end
60
+
61
+ Dir.foreach(path) do |basename|
62
+ next if basename.start_with?('.')
63
+ next unless f = files.call(basename)
64
+ yield(DIR_FILE % DIR_FILE_escape(f))
65
+ end
66
+
67
+ yield(DIR_PAGE_FOOTER)
48
68
  end
49
69
 
50
70
  private
51
- # Assumes url is already escaped.
52
- def DIR_FILE_escape url, *html
53
- [url, *html.map { |e| Utils.escape_html(e) }]
71
+
72
+ # Escape each element in the array of html strings.
73
+ def DIR_FILE_escape(htmls)
74
+ htmls.map { |e| Utils.escape_html(e) }
54
75
  end
55
76
  end
56
77
 
57
- attr_reader :root, :path
78
+ # The root of the directory hierarchy. Only requests for files and
79
+ # directories inside of the root directory are supported.
80
+ attr_reader :root
58
81
 
59
- def initialize(root, app=nil)
82
+ # Set the root directory and application for serving files.
83
+ def initialize(root, app = nil)
60
84
  @root = ::File.expand_path(root)
61
- @app = app || Rack::File.new(@root)
62
- @head = Rack::Head.new(lambda { |env| get env })
85
+ @app = app || Files.new(@root)
86
+ @head = Head.new(method(:get))
63
87
  end
64
88
 
65
89
  def call(env)
@@ -67,100 +91,101 @@ table { width:100%%; }
67
91
  @head.call env
68
92
  end
69
93
 
94
+ # Internals of request handling. Similar to call but does
95
+ # not remove body for HEAD requests.
70
96
  def get(env)
71
97
  script_name = env[SCRIPT_NAME]
72
98
  path_info = Utils.unescape_path(env[PATH_INFO])
73
99
 
74
- if bad_request = check_bad_request(path_info)
75
- bad_request
76
- elsif forbidden = check_forbidden(path_info)
77
- forbidden
100
+ if client_error_response = check_bad_request(path_info) || check_forbidden(path_info)
101
+ client_error_response
78
102
  else
79
103
  path = ::File.join(@root, path_info)
80
104
  list_path(env, path, path_info, script_name)
81
105
  end
82
106
  end
83
107
 
108
+ # Rack response to use for requests with invalid paths, or nil if path is valid.
84
109
  def check_bad_request(path_info)
85
110
  return if Utils.valid_path?(path_info)
86
111
 
87
112
  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]]
113
+ [400, { CONTENT_TYPE => "text/plain",
114
+ CONTENT_LENGTH => body.bytesize.to_s,
115
+ "x-cascade" => "pass" }, [body]]
92
116
  end
93
117
 
118
+ # Rack response to use for requests with paths outside the root, or nil if path is inside the root.
94
119
  def check_forbidden(path_info)
95
120
  return unless path_info.include? ".."
121
+ return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
96
122
 
97
123
  body = "Forbidden\n"
98
- size = body.bytesize
99
- return [403, {CONTENT_TYPE => "text/plain",
100
- CONTENT_LENGTH => size.to_s,
101
- "X-Cascade" => "pass"}, [body]]
124
+ [403, { CONTENT_TYPE => "text/plain",
125
+ CONTENT_LENGTH => body.bytesize.to_s,
126
+ "x-cascade" => "pass" }, [body]]
102
127
  end
103
128
 
129
+ # Rack response to use for directories under the root.
104
130
  def list_directory(path_info, path, script_name)
105
- files = [['../','Parent Directory','','','']]
106
- glob = ::File.join(path, '*')
107
-
108
131
  url_head = (script_name.split('/') + path_info.split('/')).map do |part|
109
- Rack::Utils.escape_path part
132
+ Utils.escape_path part
110
133
  end
111
134
 
112
- Dir[glob].sort.each do |node|
113
- stat = stat(node)
135
+ # Globbing not safe as path could contain glob metacharacters
136
+ body = DirectoryBody.new(@root, path, ->(basename) do
137
+ stat = stat(::File.join(path, basename))
114
138
  next unless stat
115
- basename = ::File.basename(node)
116
- ext = ::File.extname(node)
117
139
 
118
- url = ::File.join(*url_head + [Rack::Utils.escape_path(basename)])
119
- size = stat.size
120
- type = stat.directory? ? 'directory' : Mime.mime_type(ext)
121
- size = stat.directory? ? '-' : filesize_format(size)
140
+ url = ::File.join(*url_head + [Utils.escape_path(basename)])
122
141
  mtime = stat.mtime.httpdate
123
- url << '/' if stat.directory?
124
- basename << '/' if stat.directory?
125
-
126
- files << [ url, basename, size, type, mtime ]
127
- end
128
-
129
- return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, DirectoryBody.new(@root, path, files) ]
142
+ if stat.directory?
143
+ type = 'directory'
144
+ size = '-'
145
+ url << '/'
146
+ if basename == '..'
147
+ basename = 'Parent Directory'
148
+ else
149
+ basename << '/'
150
+ end
151
+ else
152
+ type = Mime.mime_type(::File.extname(basename))
153
+ size = filesize_format(stat.size)
154
+ end
155
+
156
+ [ url, basename, size, type, mtime ]
157
+ end)
158
+
159
+ [ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, body ]
130
160
  end
131
161
 
132
- def stat(node)
133
- ::File.stat(node)
162
+ # File::Stat for the given path, but return nil for missing/bad entries.
163
+ def stat(path)
164
+ ::File.stat(path)
134
165
  rescue Errno::ENOENT, Errno::ELOOP
135
166
  return nil
136
167
  end
137
168
 
138
- # TODO: add correct response if not readable, not sure if 404 is the best
139
- # option
169
+ # Rack response to use for files and directories under the root.
170
+ # Unreadable and non-file, non-directory entries will get a 404 response.
140
171
  def list_path(env, path, path_info, script_name)
141
- stat = ::File.stat(path)
142
-
143
- if stat.readable?
172
+ if (stat = stat(path)) && stat.readable?
144
173
  return @app.call(env) if stat.file?
145
174
  return list_directory(path_info, path, script_name) if stat.directory?
146
- else
147
- raise Errno::ENOENT, 'No such file or directory'
148
175
  end
149
176
 
150
- rescue Errno::ENOENT, Errno::ELOOP
151
- return entity_not_found(path_info)
177
+ entity_not_found(path_info)
152
178
  end
153
179
 
180
+ # Rack response to use for unreadable and non-file, non-directory entries.
154
181
  def entity_not_found(path_info)
155
182
  body = "Entity not found: #{path_info}\n"
156
- size = body.bytesize
157
- return [404, {CONTENT_TYPE => "text/plain",
158
- CONTENT_LENGTH => size.to_s,
159
- "X-Cascade" => "pass"}, [body]]
183
+ [404, { CONTENT_TYPE => "text/plain",
184
+ CONTENT_LENGTH => body.bytesize.to_s,
185
+ "x-cascade" => "pass" }, [body]]
160
186
  end
161
187
 
162
188
  # Stolen from Ramaze
163
-
164
189
  FILESIZE_FORMAT = [
165
190
  ['%.1fT', 1 << 40],
166
191
  ['%.1fG', 1 << 30],
@@ -168,6 +193,7 @@ table { width:100%%; }
168
193
  ['%.1fK', 1 << 10],
169
194
  ]
170
195
 
196
+ # Provide human readable file sizes
171
197
  def filesize_format(int)
172
198
  FILESIZE_FORMAT.each do |format, size|
173
199
  return format % (int.to_f / size) if int >= size
data/lib/rack/etag.rb CHANGED
@@ -1,19 +1,23 @@
1
- require 'rack'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'digest/sha2'
3
4
 
5
+ require_relative 'constants'
6
+ require_relative 'utils'
7
+
4
8
  module Rack
5
- # Automatically sets the ETag header on all String bodies.
9
+ # Automatically sets the etag header on all String bodies.
6
10
  #
7
- # The ETag header is skipped if ETag or Last-Modified headers are sent or if
11
+ # The etag header is skipped if etag or last-modified headers are sent or if
8
12
  # a sendfile body (body.responds_to :to_path) is given (since such cases
9
13
  # should be handled by apache/nginx).
10
14
  #
11
- # On initialization, you can pass two parameters: a Cache-Control directive
12
- # used when Etag is absent and a directive when it is present. The first
15
+ # On initialization, you can pass two parameters: a cache-control directive
16
+ # used when etag is absent and a directive when it is present. The first
13
17
  # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
14
18
  class ETag
15
19
  ETAG_STRING = Rack::ETAG
16
- DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
20
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
17
21
 
18
22
  def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
19
23
  @app = app
@@ -22,14 +26,11 @@ module Rack
22
26
  end
23
27
 
24
28
  def call(env)
25
- status, headers, body = @app.call(env)
29
+ status, headers, body = response = @app.call(env)
26
30
 
27
- if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
28
- original_body = body
29
- digest, new_body = digest_body(body)
30
- body = Rack::BodyProxy.new(new_body) do
31
- original_body.close if original_body.respond_to?(:close)
32
- end
31
+ if etag_status?(status) && body.respond_to?(:to_ary) && !skip_caching?(headers)
32
+ body = body.to_ary
33
+ digest = digest_body(body)
33
34
  headers[ETAG_STRING] = %(W/"#{digest}") if digest
34
35
  end
35
36
 
@@ -41,7 +42,7 @@ module Rack
41
42
  end
42
43
  end
43
44
 
44
- [status, headers, body]
45
+ response
45
46
  end
46
47
 
47
48
  private
@@ -50,25 +51,18 @@ module Rack
50
51
  status == 200 || status == 201
51
52
  end
52
53
 
53
- def etag_body?(body)
54
- !body.respond_to?(:to_path)
55
- end
56
-
57
54
  def skip_caching?(headers)
58
- (headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
59
- headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
55
+ headers.key?(ETAG_STRING) || headers.key?('last-modified')
60
56
  end
61
57
 
62
58
  def digest_body(body)
63
- parts = []
64
59
  digest = nil
65
60
 
66
61
  body.each do |part|
67
- parts << part
68
62
  (digest ||= Digest::SHA256.new) << part unless part.empty?
69
63
  end
70
64
 
71
- [digest && digest.hexdigest.byteslice(0, 32), parts]
65
+ digest && digest.hexdigest.byteslice(0,32)
72
66
  end
73
67
  end
74
68
  end
data/lib/rack/events.rb CHANGED
@@ -1,11 +1,14 @@
1
- require 'rack/response'
2
- require 'rack/body_proxy'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'body_proxy'
4
+ require_relative 'request'
5
+ require_relative 'response'
3
6
 
4
7
  module Rack
5
8
  ### 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
+ # response lifecycle. This is so that middleware that don't need to filter
10
+ # the response data can safely leave it alone and not have to send messages
11
+ # down the traditional "rack stack".
9
12
  #
10
13
  # The events are:
11
14
  #
@@ -57,26 +60,26 @@ module Rack
57
60
 
58
61
  class Events
59
62
  module Abstract
60
- def on_start req, res
63
+ def on_start(req, res)
61
64
  end
62
65
 
63
- def on_commit req, res
66
+ def on_commit(req, res)
64
67
  end
65
68
 
66
- def on_send req, res
69
+ def on_send(req, res)
67
70
  end
68
71
 
69
- def on_finish req, res
72
+ def on_finish(req, res)
70
73
  end
71
74
 
72
- def on_error req, res, e
75
+ def on_error(req, res, e)
73
76
  end
74
77
  end
75
78
 
76
79
  class EventedBodyProxy < Rack::BodyProxy # :nodoc:
77
80
  attr_reader :request, :response
78
81
 
79
- def initialize body, request, response, handlers, &block
82
+ def initialize(body, request, response, handlers, &block)
80
83
  super(body, &block)
81
84
  @request = request
82
85
  @response = response
@@ -92,7 +95,7 @@ module Rack
92
95
  class BufferedResponse < Rack::Response::Raw # :nodoc:
93
96
  attr_reader :body
94
97
 
95
- def initialize status, headers, body
98
+ def initialize(status, headers, body)
96
99
  super(status, headers)
97
100
  @body = body
98
101
  end
@@ -100,12 +103,12 @@ module Rack
100
103
  def to_a; [status, headers, body]; end
101
104
  end
102
105
 
103
- def initialize app, handlers
106
+ def initialize(app, handlers)
104
107
  @app = app
105
108
  @handlers = handlers
106
109
  end
107
110
 
108
- def call env
111
+ def call(env)
109
112
  request = make_request env
110
113
  on_start request, nil
111
114
 
@@ -127,27 +130,27 @@ module Rack
127
130
 
128
131
  private
129
132
 
130
- def on_error request, response, e
133
+ def on_error(request, response, e)
131
134
  @handlers.reverse_each { |handler| handler.on_error request, response, e }
132
135
  end
133
136
 
134
- def on_commit request, response
137
+ def on_commit(request, response)
135
138
  @handlers.reverse_each { |handler| handler.on_commit request, response }
136
139
  end
137
140
 
138
- def on_start request, response
141
+ def on_start(request, response)
139
142
  @handlers.each { |handler| handler.on_start request, nil }
140
143
  end
141
144
 
142
- def on_finish request, response
145
+ def on_finish(request, response)
143
146
  @handlers.reverse_each { |handler| handler.on_finish request, response }
144
147
  end
145
148
 
146
- def make_request env
149
+ def make_request(env)
147
150
  Rack::Request.new env
148
151
  end
149
152
 
150
- def make_response status, headers, body
153
+ def make_response(status, headers, body)
151
154
  BufferedResponse.new status, headers, body
152
155
  end
153
156
  end