rack 1.4.7 → 2.1.4

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 (183) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +77 -0
  3. data/{COPYING → MIT-LICENSE} +4 -2
  4. data/README.rdoc +122 -456
  5. data/Rakefile +32 -31
  6. data/SPEC +119 -29
  7. data/bin/rackup +1 -0
  8. data/contrib/rack_logo.svg +164 -111
  9. data/example/lobster.ru +2 -0
  10. data/example/protectedlobster.rb +4 -2
  11. data/example/protectedlobster.ru +3 -1
  12. data/lib/rack/auth/abstract/handler.rb +7 -5
  13. data/lib/rack/auth/abstract/request.rb +8 -6
  14. data/lib/rack/auth/basic.rb +5 -2
  15. data/lib/rack/auth/digest/md5.rb +10 -8
  16. data/lib/rack/auth/digest/nonce.rb +6 -3
  17. data/lib/rack/auth/digest/params.rb +5 -4
  18. data/lib/rack/auth/digest/request.rb +4 -2
  19. data/lib/rack/body_proxy.rb +11 -9
  20. data/lib/rack/builder.rb +63 -20
  21. data/lib/rack/cascade.rb +10 -9
  22. data/lib/rack/chunked.rb +45 -11
  23. data/lib/rack/{commonlogger.rb → common_logger.rb} +24 -15
  24. data/lib/rack/{conditionalget.rb → conditional_get.rb} +20 -6
  25. data/lib/rack/config.rb +7 -0
  26. data/lib/rack/content_length.rb +12 -6
  27. data/lib/rack/content_type.rb +4 -2
  28. data/lib/rack/core_ext/regexp.rb +14 -0
  29. data/lib/rack/deflater.rb +73 -42
  30. data/lib/rack/directory.rb +77 -56
  31. data/lib/rack/etag.rb +25 -13
  32. data/lib/rack/events.rb +156 -0
  33. data/lib/rack/file.rb +4 -143
  34. data/lib/rack/files.rb +178 -0
  35. data/lib/rack/handler/cgi.rb +18 -17
  36. data/lib/rack/handler/fastcgi.rb +21 -17
  37. data/lib/rack/handler/lsws.rb +14 -12
  38. data/lib/rack/handler/scgi.rb +27 -21
  39. data/lib/rack/handler/thin.rb +19 -5
  40. data/lib/rack/handler/webrick.rb +66 -24
  41. data/lib/rack/handler.rb +29 -19
  42. data/lib/rack/head.rb +21 -14
  43. data/lib/rack/lint.rb +259 -65
  44. data/lib/rack/lobster.rb +17 -10
  45. data/lib/rack/lock.rb +19 -10
  46. data/lib/rack/logger.rb +4 -2
  47. data/lib/rack/media_type.rb +43 -0
  48. data/lib/rack/method_override.rb +52 -0
  49. data/lib/rack/mime.rb +43 -6
  50. data/lib/rack/mock.rb +109 -44
  51. data/lib/rack/multipart/generator.rb +11 -12
  52. data/lib/rack/multipart/parser.rb +302 -115
  53. data/lib/rack/multipart/uploaded_file.rb +4 -3
  54. data/lib/rack/multipart.rb +40 -9
  55. data/lib/rack/null_logger.rb +39 -0
  56. data/lib/rack/query_parser.rb +218 -0
  57. data/lib/rack/recursive.rb +14 -11
  58. data/lib/rack/reloader.rb +12 -5
  59. data/lib/rack/request.rb +484 -270
  60. data/lib/rack/response.rb +196 -77
  61. data/lib/rack/rewindable_input.rb +5 -14
  62. data/lib/rack/runtime.rb +13 -6
  63. data/lib/rack/sendfile.rb +44 -20
  64. data/lib/rack/server.rb +175 -61
  65. data/lib/rack/session/abstract/id.rb +276 -133
  66. data/lib/rack/session/cookie.rb +75 -40
  67. data/lib/rack/session/memcache.rb +4 -87
  68. data/lib/rack/session/pool.rb +24 -18
  69. data/lib/rack/show_exceptions.rb +392 -0
  70. data/lib/rack/{showstatus.rb → show_status.rb} +11 -9
  71. data/lib/rack/static.rb +65 -38
  72. data/lib/rack/tempfile_reaper.rb +24 -0
  73. data/lib/rack/urlmap.rb +40 -15
  74. data/lib/rack/utils.rb +316 -285
  75. data/lib/rack.rb +78 -23
  76. data/rack.gemspec +26 -19
  77. metadata +44 -209
  78. data/KNOWN-ISSUES +0 -30
  79. data/lib/rack/backports/uri/common_18.rb +0 -56
  80. data/lib/rack/backports/uri/common_192.rb +0 -52
  81. data/lib/rack/backports/uri/common_193.rb +0 -29
  82. data/lib/rack/handler/evented_mongrel.rb +0 -8
  83. data/lib/rack/handler/mongrel.rb +0 -100
  84. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  85. data/lib/rack/methodoverride.rb +0 -33
  86. data/lib/rack/nulllogger.rb +0 -18
  87. data/lib/rack/showexceptions.rb +0 -378
  88. data/test/builder/anything.rb +0 -5
  89. data/test/builder/comment.ru +0 -4
  90. data/test/builder/end.ru +0 -5
  91. data/test/builder/line.ru +0 -1
  92. data/test/builder/options.ru +0 -2
  93. data/test/cgi/assets/folder/test.js +0 -1
  94. data/test/cgi/assets/fonts/font.eot +0 -1
  95. data/test/cgi/assets/images/image.png +0 -1
  96. data/test/cgi/assets/index.html +0 -1
  97. data/test/cgi/assets/javascripts/app.js +0 -1
  98. data/test/cgi/assets/stylesheets/app.css +0 -1
  99. data/test/cgi/lighttpd.conf +0 -26
  100. data/test/cgi/lighttpd.errors +0 -1
  101. data/test/cgi/rackup_stub.rb +0 -6
  102. data/test/cgi/sample_rackup.ru +0 -5
  103. data/test/cgi/test +0 -9
  104. data/test/cgi/test+directory/test+file +0 -1
  105. data/test/cgi/test.fcgi +0 -8
  106. data/test/cgi/test.ru +0 -5
  107. data/test/gemloader.rb +0 -10
  108. data/test/multipart/bad_robots +0 -259
  109. data/test/multipart/binary +0 -0
  110. data/test/multipart/content_type_and_no_filename +0 -6
  111. data/test/multipart/empty +0 -10
  112. data/test/multipart/fail_16384_nofile +0 -814
  113. data/test/multipart/file1.txt +0 -1
  114. data/test/multipart/filename_and_modification_param +0 -7
  115. data/test/multipart/filename_with_escaped_quotes +0 -6
  116. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  117. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  118. data/test/multipart/filename_with_unescaped_percentages +0 -6
  119. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  120. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  121. data/test/multipart/filename_with_unescaped_quotes +0 -6
  122. data/test/multipart/ie +0 -6
  123. data/test/multipart/mixed_files +0 -21
  124. data/test/multipart/nested +0 -10
  125. data/test/multipart/none +0 -9
  126. data/test/multipart/semicolon +0 -6
  127. data/test/multipart/text +0 -15
  128. data/test/multipart/three_files_three_fields +0 -31
  129. data/test/multipart/webkit +0 -32
  130. data/test/rackup/config.ru +0 -31
  131. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  132. data/test/spec_auth.rb +0 -57
  133. data/test/spec_auth_basic.rb +0 -81
  134. data/test/spec_auth_digest.rb +0 -259
  135. data/test/spec_body_proxy.rb +0 -69
  136. data/test/spec_builder.rb +0 -207
  137. data/test/spec_cascade.rb +0 -61
  138. data/test/spec_cgi.rb +0 -102
  139. data/test/spec_chunked.rb +0 -87
  140. data/test/spec_commonlogger.rb +0 -57
  141. data/test/spec_conditionalget.rb +0 -102
  142. data/test/spec_config.rb +0 -22
  143. data/test/spec_content_length.rb +0 -86
  144. data/test/spec_content_type.rb +0 -45
  145. data/test/spec_deflater.rb +0 -187
  146. data/test/spec_directory.rb +0 -88
  147. data/test/spec_etag.rb +0 -98
  148. data/test/spec_fastcgi.rb +0 -107
  149. data/test/spec_file.rb +0 -200
  150. data/test/spec_handler.rb +0 -59
  151. data/test/spec_head.rb +0 -48
  152. data/test/spec_lint.rb +0 -515
  153. data/test/spec_lobster.rb +0 -58
  154. data/test/spec_lock.rb +0 -167
  155. data/test/spec_logger.rb +0 -23
  156. data/test/spec_methodoverride.rb +0 -72
  157. data/test/spec_mock.rb +0 -269
  158. data/test/spec_mongrel.rb +0 -182
  159. data/test/spec_multipart.rb +0 -479
  160. data/test/spec_nulllogger.rb +0 -23
  161. data/test/spec_recursive.rb +0 -72
  162. data/test/spec_request.rb +0 -955
  163. data/test/spec_response.rb +0 -313
  164. data/test/spec_rewindable_input.rb +0 -118
  165. data/test/spec_runtime.rb +0 -49
  166. data/test/spec_sendfile.rb +0 -90
  167. data/test/spec_server.rb +0 -121
  168. data/test/spec_session_abstract_id.rb +0 -43
  169. data/test/spec_session_cookie.rb +0 -361
  170. data/test/spec_session_memcache.rb +0 -321
  171. data/test/spec_session_pool.rb +0 -209
  172. data/test/spec_showexceptions.rb +0 -92
  173. data/test/spec_showstatus.rb +0 -84
  174. data/test/spec_static.rb +0 -145
  175. data/test/spec_thin.rb +0 -86
  176. data/test/spec_urlmap.rb +0 -213
  177. data/test/spec_utils.rb +0 -554
  178. data/test/spec_webrick.rb +0 -143
  179. data/test/static/another/index.html +0 -1
  180. data/test/static/index.html +0 -1
  181. data/test/testrequest.rb +0 -78
  182. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  183. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/deflater.rb CHANGED
@@ -1,32 +1,61 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "zlib"
2
- require "stringio"
3
4
  require "time" # for Time.httpdate
4
5
  require 'rack/utils'
5
6
 
7
+ require_relative 'core_ext/regexp'
8
+
6
9
  module Rack
10
+ # This middleware enables compression of http responses.
11
+ #
12
+ # Currently supported compression algorithms:
13
+ #
14
+ # * gzip
15
+ # * identity (no transformation)
16
+ #
17
+ # The middleware automatically detects when compression is supported
18
+ # and allowed. For example no transformation is made when a cache
19
+ # directive of 'no-transform' is present, or when the response status
20
+ # code is one that doesn't allow an entity body.
7
21
  class Deflater
8
- def initialize(app)
22
+ using ::Rack::RegexpExtensions
23
+
24
+ ##
25
+ # Creates Rack::Deflater middleware.
26
+ #
27
+ # [app] rack app instance
28
+ # [options] hash of deflater options, i.e.
29
+ # 'if' - a lambda enabling / disabling deflation based on returned boolean value
30
+ # e.g use Rack::Deflater, :if => lambda { |*, body| sum=0; body.each { |i| sum += i.length }; sum > 512 }
31
+ # 'include' - a list of content types that should be compressed
32
+ # 'sync' - determines if the stream is going to be flushed after every chunk.
33
+ # Flushing after every chunk reduces latency for
34
+ # time-sensitive streaming applications, but hurts
35
+ # compression and throughput. Defaults to `true'.
36
+ def initialize(app, options = {})
9
37
  @app = app
38
+
39
+ @condition = options[:if]
40
+ @compressible_types = options[:include]
41
+ @sync = options[:sync] == false ? false : true
10
42
  end
11
43
 
12
44
  def call(env)
13
45
  status, headers, body = @app.call(env)
14
46
  headers = Utils::HeaderHash.new(headers)
15
47
 
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/
48
+ unless should_deflate?(env, status, headers, body)
20
49
  return [status, headers, body]
21
50
  end
22
51
 
23
52
  request = Request.new(env)
24
53
 
25
- encoding = Utils.select_best_encoding(%w(gzip deflate identity),
54
+ encoding = Utils.select_best_encoding(%w(gzip identity),
26
55
  request.accept_encoding)
27
56
 
28
57
  # Set the Vary HTTP header.
29
- vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
58
+ vary = headers["Vary"].to_s.split(",").map(&:strip)
30
59
  unless vary.include?("*") || vary.include?("Accept-Encoding")
31
60
  headers["Vary"] = vary.push("Accept-Encoding").join(",")
32
61
  end
@@ -35,38 +64,35 @@ module Rack
35
64
  when "gzip"
36
65
  headers['Content-Encoding'] = "gzip"
37
66
  headers.delete('Content-Length')
38
- mtime = headers.key?("Last-Modified") ?
39
- Time.httpdate(headers["Last-Modified"]) : Time.now
40
- [status, headers, GzipStream.new(body, mtime)]
41
- when "deflate"
42
- headers['Content-Encoding'] = "deflate"
43
- headers.delete('Content-Length')
44
- [status, headers, DeflateStream.new(body)]
67
+ mtime = headers["Last-Modified"]
68
+ mtime = Time.httpdate(mtime).to_i if mtime
69
+ [status, headers, GzipStream.new(body, mtime, @sync)]
45
70
  when "identity"
46
71
  [status, headers, body]
47
72
  when nil
48
- body.close if body.respond_to?(:close)
49
73
  message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
50
- [406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]]
74
+ bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
75
+ [406, { 'Content-Type' => "text/plain", 'Content-Length' => message.length.to_s }, bp]
51
76
  end
52
77
  end
53
78
 
54
79
  class GzipStream
55
- def initialize(body, mtime)
80
+ def initialize(body, mtime, sync)
81
+ @sync = sync
56
82
  @body = body
57
83
  @mtime = mtime
58
84
  end
59
85
 
60
86
  def each(&block)
61
87
  @writer = block
62
- gzip =::Zlib::GzipWriter.new(self)
63
- gzip.mtime = @mtime
88
+ gzip = ::Zlib::GzipWriter.new(self)
89
+ gzip.mtime = @mtime if @mtime
64
90
  @body.each { |part|
65
- gzip.write(part)
66
- gzip.flush
91
+ len = gzip.write(part)
92
+ # Flushing empty parts would raise Zlib::BufError.
93
+ gzip.flush if @sync && len > 0
67
94
  }
68
95
  ensure
69
- @body.close if @body.respond_to?(:close)
70
96
  gzip.close
71
97
  @writer = nil
72
98
  end
@@ -74,30 +100,35 @@ module Rack
74
100
  def write(data)
75
101
  @writer.call(data)
76
102
  end
103
+
104
+ def close
105
+ @body.close if @body.respond_to?(:close)
106
+ @body = nil
107
+ end
77
108
  end
78
109
 
79
- class DeflateStream
80
- DEFLATE_ARGS = [
81
- Zlib::DEFAULT_COMPRESSION,
82
- # drop the zlib header which causes both Safari and IE to choke
83
- -Zlib::MAX_WBITS,
84
- Zlib::DEF_MEM_LEVEL,
85
- Zlib::DEFAULT_STRATEGY
86
- ]
110
+ private
87
111
 
88
- def initialize(body)
89
- @body = body
112
+ def should_deflate?(env, status, headers, body)
113
+ # Skip compressing empty entity body responses and responses with
114
+ # no-transform set.
115
+ if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
116
+ /\bno-transform\b/.match?(headers['Cache-Control'].to_s) ||
117
+ (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
118
+ return false
90
119
  end
91
120
 
92
- def each
93
- deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
94
- @body.each { |part| yield deflater.deflate(part, Zlib::SYNC_FLUSH) }
95
- yield deflater.finish
96
- nil
97
- ensure
98
- @body.close if @body.respond_to?(:close)
99
- deflater.close
100
- end
121
+ # Skip if @compressible_types are given and does not include request's content type
122
+ return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/]))
123
+
124
+ # Skip if @condition lambda is given and evaluates to false
125
+ return false if @condition && !@condition.call(env, status, headers, body)
126
+
127
+ # No point in compressing empty body, also handles usage with
128
+ # Rack::Sendfile.
129
+ return false if headers[CONTENT_LENGTH] == '0'
130
+
131
+ true
101
132
  end
102
133
  end
103
134
  end
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'time'
2
4
  require 'rack/utils'
3
5
  require 'rack/mime'
6
+ require 'rack/files'
4
7
 
5
8
  module Rack
6
9
  # Rack::Directory serves entries below the +root+ given, according to the
@@ -8,7 +11,7 @@ module Rack
8
11
  # will be presented in an html based index. If a file is found, the env will
9
12
  # be passed to the specified +app+.
10
13
  #
11
- # If +app+ is not specified, a Rack::File of the same +root+ will be used.
14
+ # If +app+ is not specified, a Rack::Files of the same +root+ will be used.
12
15
 
13
16
  class Directory
14
17
  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>"
@@ -39,58 +42,83 @@ table { width:100%%; }
39
42
  </body></html>
40
43
  PAGE
41
44
 
42
- attr_reader :files
43
- attr_accessor :root, :path
45
+ class DirectoryBody < Struct.new(:root, :path, :files)
46
+ def each
47
+ show_path = Rack::Utils.escape_html(path.sub(/^#{root}/, ''))
48
+ listings = files.map{|f| DIR_FILE % DIR_FILE_escape(*f) } * "\n"
49
+ page = DIR_PAGE % [ show_path, show_path, listings ]
50
+ page.each_line{|l| yield l }
51
+ end
44
52
 
45
- def initialize(root, app=nil)
46
- @root = F.expand_path(root)
47
- @app = app || Rack::File.new(@root)
53
+ private
54
+ # Assumes url is already escaped.
55
+ def DIR_FILE_escape url, *html
56
+ [url, *html.map { |e| Utils.escape_html(e) }]
57
+ end
48
58
  end
49
59
 
50
- def call(env)
51
- dup._call(env)
60
+ attr_reader :root, :path
61
+
62
+ def initialize(root, app = nil)
63
+ @root = ::File.expand_path(root)
64
+ @app = app || Rack::Files.new(@root)
65
+ @head = Rack::Head.new(lambda { |env| get env })
52
66
  end
53
67
 
54
- F = ::File
68
+ def call(env)
69
+ # strip body if this is a HEAD call
70
+ @head.call env
71
+ end
55
72
 
56
- def _call(env)
57
- @env = env
58
- @script_name = env['SCRIPT_NAME']
59
- @path_info = Utils.unescape(env['PATH_INFO'])
73
+ def get(env)
74
+ script_name = env[SCRIPT_NAME]
75
+ path_info = Utils.unescape_path(env[PATH_INFO])
60
76
 
61
- if forbidden = check_forbidden
77
+ if bad_request = check_bad_request(path_info)
78
+ bad_request
79
+ elsif forbidden = check_forbidden(path_info)
62
80
  forbidden
63
81
  else
64
- @path = F.join(@root, @path_info)
65
- list_path
82
+ path = ::File.join(@root, path_info)
83
+ list_path(env, path, path_info, script_name)
66
84
  end
67
85
  end
68
86
 
69
- def check_forbidden
70
- return unless @path_info.include? ".."
87
+ def check_bad_request(path_info)
88
+ return if Utils.valid_path?(path_info)
89
+
90
+ body = "Bad Request\n"
91
+ size = body.bytesize
92
+ return [400, { CONTENT_TYPE => "text/plain",
93
+ CONTENT_LENGTH => size.to_s,
94
+ "X-Cascade" => "pass" }, [body]]
95
+ end
96
+
97
+ def check_forbidden(path_info)
98
+ return unless path_info.include? ".."
71
99
 
72
100
  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]]
101
+ size = body.bytesize
102
+ return [403, { CONTENT_TYPE => "text/plain",
103
+ CONTENT_LENGTH => size.to_s,
104
+ "X-Cascade" => "pass" }, [body]]
77
105
  end
78
106
 
79
- def list_directory
80
- @files = [['../','Parent Directory','','','']]
81
- glob = F.join(@path, '*')
107
+ def list_directory(path_info, path, script_name)
108
+ files = [['../', 'Parent Directory', '', '', '']]
82
109
 
83
- url_head = (@script_name.split('/') + @path_info.split('/')).map do |part|
84
- Rack::Utils.escape part
110
+ url_head = (script_name.split('/') + path_info.split('/')).map do |part|
111
+ Rack::Utils.escape_path part
85
112
  end
86
113
 
87
- Dir[glob].sort.each do |node|
114
+ Dir.entries(path).reject { |e| e.start_with?('.') }.sort.each do |node|
115
+ node = ::File.join path, node
88
116
  stat = stat(node)
89
- next unless stat
90
- basename = F.basename(node)
91
- ext = F.extname(node)
117
+ next unless stat
118
+ basename = ::File.basename(node)
119
+ ext = ::File.extname(node)
92
120
 
93
- url = F.join(*url_head + [Rack::Utils.escape(basename)])
121
+ url = ::File.join(*url_head + [Rack::Utils.escape_path(basename)])
94
122
  size = stat.size
95
123
  type = stat.directory? ? 'directory' : Mime.mime_type(ext)
96
124
  size = stat.directory? ? '-' : filesize_format(size)
@@ -98,47 +126,40 @@ table { width:100%%; }
98
126
  url << '/' if stat.directory?
99
127
  basename << '/' if stat.directory?
100
128
 
101
- @files << [ url, basename, size, type, mtime ]
129
+ files << [ url, basename, size, type, mtime ]
102
130
  end
103
131
 
104
- return [ 200, {'Content-Type'=>'text/html; charset=utf-8'}, self ]
132
+ return [ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, DirectoryBody.new(@root, path, files) ]
105
133
  end
106
134
 
107
- def stat(node, max = 10)
108
- F.stat(node)
135
+ def stat(node)
136
+ ::File.stat(node)
109
137
  rescue Errno::ENOENT, Errno::ELOOP
110
138
  return nil
111
139
  end
112
140
 
113
141
  # TODO: add correct response if not readable, not sure if 404 is the best
114
142
  # option
115
- def list_path
116
- @stat = F.stat(@path)
143
+ def list_path(env, path, path_info, script_name)
144
+ stat = ::File.stat(path)
117
145
 
118
- if @stat.readable?
119
- return @app.call(@env) if @stat.file?
120
- return list_directory if @stat.directory?
146
+ if stat.readable?
147
+ return @app.call(env) if stat.file?
148
+ return list_directory(path_info, path, script_name) if stat.directory?
121
149
  else
122
150
  raise Errno::ENOENT, 'No such file or directory'
123
151
  end
124
152
 
125
153
  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]]
154
+ return entity_not_found(path_info)
135
155
  end
136
156
 
137
- def each
138
- show_path = @path.sub(/^#{@root}/,'')
139
- files = @files.map{|f| DIR_FILE % f }*"\n"
140
- page = DIR_PAGE % [ show_path, show_path , files ]
141
- page.each_line{|l| yield l }
157
+ def entity_not_found(path_info)
158
+ body = "Entity not found: #{path_info}\n"
159
+ size = body.bytesize
160
+ return [404, { CONTENT_TYPE => "text/plain",
161
+ CONTENT_LENGTH => size.to_s,
162
+ "X-Cascade" => "pass" }, [body]]
142
163
  end
143
164
 
144
165
  # Stolen from Ramaze
@@ -155,7 +176,7 @@ table { width:100%%; }
155
176
  return format % (int.to_f / size) if int >= size
156
177
  end
157
178
 
158
- int.to_s + 'B'
179
+ "#{int}B"
159
180
  end
160
181
  end
161
182
  end
data/lib/rack/etag.rb CHANGED
@@ -1,4 +1,7 @@
1
- require 'digest/md5'
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'digest/sha2'
2
5
 
3
6
  module Rack
4
7
  # Automatically sets the ETag header on all String bodies.
@@ -11,7 +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
- 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"
15
19
 
16
20
  def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
17
21
  @app = app
@@ -23,15 +27,19 @@ module Rack
23
27
  status, headers, body = @app.call(env)
24
28
 
25
29
  if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
26
- digest, body = digest_body(body)
27
- headers['ETag'] = %("#{digest}") if digest
30
+ original_body = body
31
+ digest, new_body = digest_body(body)
32
+ body = Rack::BodyProxy.new(new_body) do
33
+ original_body.close if original_body.respond_to?(:close)
34
+ end
35
+ headers[ETAG_STRING] = %(W/"#{digest}") if digest
28
36
  end
29
37
 
30
- unless headers['Cache-Control']
38
+ unless headers[CACHE_CONTROL]
31
39
  if digest
32
- headers['Cache-Control'] = @cache_control if @cache_control
40
+ headers[CACHE_CONTROL] = @cache_control if @cache_control
33
41
  else
34
- headers['Cache-Control'] = @no_cache_control if @no_cache_control
42
+ headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
35
43
  end
36
44
  end
37
45
 
@@ -49,16 +57,20 @@ module Rack
49
57
  end
50
58
 
51
59
  def skip_caching?(headers)
52
- (headers['Cache-Control'] && headers['Cache-Control'].include?('no-cache')) ||
53
- headers.key?('ETag') || headers.key?('Last-Modified')
60
+ (headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
61
+ headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
54
62
  end
55
63
 
56
64
  def digest_body(body)
57
65
  parts = []
58
- body.each { |part| parts << part }
59
- string_body = parts.join
60
- digest = Digest::MD5.hexdigest(string_body) unless string_body.empty?
61
- [digest, parts]
66
+ digest = nil
67
+
68
+ body.each do |part|
69
+ parts << part
70
+ (digest ||= Digest::SHA256.new) << part unless part.empty?
71
+ end
72
+
73
+ [digest && digest.hexdigest.byteslice(0, 32), parts]
62
74
  end
63
75
  end
64
76
  end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/response'
4
+ require 'rack/body_proxy'
5
+
6
+ module Rack
7
+ ### This middleware provides hooks to certain places in the request /
8
+ # response lifecycle. This is so that middleware that don't need to filter
9
+ # the response data can safely leave it alone and not have to send messages
10
+ # down the traditional "rack stack".
11
+ #
12
+ # The events are:
13
+ #
14
+ # * on_start(request, response)
15
+ #
16
+ # This event is sent at the start of the request, before the next
17
+ # middleware in the chain is called. This method is called with a request
18
+ # object, and a response object. Right now, the response object is always
19
+ # nil, but in the future it may actually be a real response object.
20
+ #
21
+ # * on_commit(request, response)
22
+ #
23
+ # The response has been committed. The application has returned, but the
24
+ # response has not been sent to the webserver yet. This method is always
25
+ # called with a request object and the response object. The response
26
+ # object is constructed from the rack triple that the application returned.
27
+ # Changes may still be made to the response object at this point.
28
+ #
29
+ # * on_send(request, response)
30
+ #
31
+ # The webserver has started iterating over the response body and presumably
32
+ # has started sending data over the wire. This method is always called with
33
+ # a request object and the response object. The response object is
34
+ # constructed from the rack triple that the application returned. Changes
35
+ # SHOULD NOT be made to the response object as the webserver has already
36
+ # started sending data. Any mutations will likely result in an exception.
37
+ #
38
+ # * on_finish(request, response)
39
+ #
40
+ # The webserver has closed the response, and all data has been written to
41
+ # the response socket. The request and response object should both be
42
+ # read-only at this point. The body MAY NOT be available on the response
43
+ # object as it may have been flushed to the socket.
44
+ #
45
+ # * on_error(request, response, error)
46
+ #
47
+ # An exception has occurred in the application or an `on_commit` event.
48
+ # This method will get the request, the response (if available) and the
49
+ # exception that was raised.
50
+ #
51
+ # ## Order
52
+ #
53
+ # `on_start` is called on the handlers in the order that they were passed to
54
+ # the constructor. `on_commit`, on_send`, `on_finish`, and `on_error` are
55
+ # called in the reverse order. `on_finish` handlers are called inside an
56
+ # `ensure` block, so they are guaranteed to be called even if something
57
+ # raises an exception. If something raises an exception in a `on_finish`
58
+ # method, then nothing is guaranteed.
59
+
60
+ class Events
61
+ module Abstract
62
+ def on_start req, res
63
+ end
64
+
65
+ def on_commit req, res
66
+ end
67
+
68
+ def on_send req, res
69
+ end
70
+
71
+ def on_finish req, res
72
+ end
73
+
74
+ def on_error req, res, e
75
+ end
76
+ end
77
+
78
+ class EventedBodyProxy < Rack::BodyProxy # :nodoc:
79
+ attr_reader :request, :response
80
+
81
+ def initialize body, request, response, handlers, &block
82
+ super(body, &block)
83
+ @request = request
84
+ @response = response
85
+ @handlers = handlers
86
+ end
87
+
88
+ def each
89
+ @handlers.reverse_each { |handler| handler.on_send request, response }
90
+ super
91
+ end
92
+ end
93
+
94
+ class BufferedResponse < Rack::Response::Raw # :nodoc:
95
+ attr_reader :body
96
+
97
+ def initialize status, headers, body
98
+ super(status, headers)
99
+ @body = body
100
+ end
101
+
102
+ def to_a; [status, headers, body]; end
103
+ end
104
+
105
+ def initialize app, handlers
106
+ @app = app
107
+ @handlers = handlers
108
+ end
109
+
110
+ def call env
111
+ request = make_request env
112
+ on_start request, nil
113
+
114
+ begin
115
+ status, headers, body = @app.call request.env
116
+ response = make_response status, headers, body
117
+ on_commit request, response
118
+ rescue StandardError => e
119
+ on_error request, response, e
120
+ on_finish request, response
121
+ raise
122
+ end
123
+
124
+ body = EventedBodyProxy.new(body, request, response, @handlers) do
125
+ on_finish request, response
126
+ end
127
+ [response.status, response.headers, body]
128
+ end
129
+
130
+ private
131
+
132
+ def on_error request, response, e
133
+ @handlers.reverse_each { |handler| handler.on_error request, response, e }
134
+ end
135
+
136
+ def on_commit request, response
137
+ @handlers.reverse_each { |handler| handler.on_commit request, response }
138
+ end
139
+
140
+ def on_start request, response
141
+ @handlers.each { |handler| handler.on_start request, nil }
142
+ end
143
+
144
+ def on_finish request, response
145
+ @handlers.reverse_each { |handler| handler.on_finish request, response }
146
+ end
147
+
148
+ def make_request env
149
+ Rack::Request.new env
150
+ end
151
+
152
+ def make_response status, headers, body
153
+ BufferedResponse.new status, headers, body
154
+ end
155
+ end
156
+ end