rack 2.0.9.3 → 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 (191) 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 +152 -162
  6. data/Rakefile +37 -23
  7. data/{SPEC → SPEC.rdoc} +44 -15
  8. data/bin/rackup +1 -0
  9. data/example/lobster.ru +2 -0
  10. data/example/protectedlobster.rb +3 -1
  11. data/example/protectedlobster.ru +2 -0
  12. data/lib/rack/auth/abstract/handler.rb +3 -1
  13. data/lib/rack/auth/abstract/request.rb +1 -1
  14. data/lib/rack/auth/basic.rb +7 -4
  15. data/lib/rack/auth/digest/md5.rb +13 -11
  16. data/lib/rack/auth/digest/nonce.rb +6 -3
  17. data/lib/rack/auth/digest/params.rb +4 -2
  18. data/lib/rack/auth/digest/request.rb +5 -3
  19. data/lib/rack/body_proxy.rb +15 -14
  20. data/lib/rack/builder.rb +116 -23
  21. data/lib/rack/cascade.rb +28 -12
  22. data/lib/rack/chunked.rb +68 -20
  23. data/lib/rack/common_logger.rb +33 -28
  24. data/lib/rack/conditional_get.rb +20 -16
  25. data/lib/rack/config.rb +2 -0
  26. data/lib/rack/content_length.rb +8 -7
  27. data/lib/rack/content_type.rb +5 -4
  28. data/lib/rack/core_ext/regexp.rb +14 -0
  29. data/lib/rack/deflater.rb +59 -34
  30. data/lib/rack/directory.rb +84 -64
  31. data/lib/rack/etag.rb +5 -4
  32. data/lib/rack/events.rb +19 -20
  33. data/lib/rack/file.rb +4 -173
  34. data/lib/rack/files.rb +218 -0
  35. data/lib/rack/handler/cgi.rb +2 -3
  36. data/lib/rack/handler/fastcgi.rb +4 -4
  37. data/lib/rack/handler/lsws.rb +3 -3
  38. data/lib/rack/handler/scgi.rb +9 -8
  39. data/lib/rack/handler/thin.rb +17 -11
  40. data/lib/rack/handler/webrick.rb +15 -6
  41. data/lib/rack/handler.rb +7 -2
  42. data/lib/rack/head.rb +1 -1
  43. data/lib/rack/lint.rb +72 -26
  44. data/lib/rack/lobster.rb +10 -10
  45. data/lib/rack/lock.rb +2 -1
  46. data/lib/rack/logger.rb +2 -0
  47. data/lib/rack/media_type.rb +10 -5
  48. data/lib/rack/method_override.rb +4 -2
  49. data/lib/rack/mime.rb +9 -1
  50. data/lib/rack/mock.rb +97 -20
  51. data/lib/rack/multipart/generator.rb +17 -13
  52. data/lib/rack/multipart/parser.rb +58 -73
  53. data/lib/rack/multipart/uploaded_file.rb +15 -7
  54. data/lib/rack/multipart.rb +7 -4
  55. data/lib/rack/null_logger.rb +2 -0
  56. data/lib/rack/query_parser.rb +53 -28
  57. data/lib/rack/recursive.rb +7 -5
  58. data/lib/rack/reloader.rb +8 -4
  59. data/lib/rack/request.rb +210 -61
  60. data/lib/rack/response.rb +127 -44
  61. data/lib/rack/rewindable_input.rb +4 -3
  62. data/lib/rack/runtime.rb +6 -4
  63. data/lib/rack/sendfile.rb +13 -9
  64. data/lib/rack/server.rb +95 -24
  65. data/lib/rack/session/abstract/id.rb +33 -21
  66. data/lib/rack/session/cookie.rb +12 -12
  67. data/lib/rack/session/memcache.rb +4 -93
  68. data/lib/rack/session/pool.rb +5 -3
  69. data/lib/rack/show_exceptions.rb +17 -13
  70. data/lib/rack/show_status.rb +5 -5
  71. data/lib/rack/static.rb +23 -11
  72. data/lib/rack/tempfile_reaper.rb +1 -1
  73. data/lib/rack/urlmap.rb +12 -6
  74. data/lib/rack/utils.rb +105 -130
  75. data/lib/rack/version.rb +29 -0
  76. data/lib/rack.rb +67 -73
  77. data/rack.gemspec +40 -28
  78. metadata +39 -182
  79. data/HISTORY.md +0 -520
  80. data/test/builder/an_underscore_app.rb +0 -5
  81. data/test/builder/anything.rb +0 -5
  82. data/test/builder/comment.ru +0 -4
  83. data/test/builder/end.ru +0 -5
  84. data/test/builder/line.ru +0 -1
  85. data/test/builder/options.ru +0 -2
  86. data/test/cgi/assets/folder/test.js +0 -1
  87. data/test/cgi/assets/fonts/font.eot +0 -1
  88. data/test/cgi/assets/images/image.png +0 -1
  89. data/test/cgi/assets/index.html +0 -1
  90. data/test/cgi/assets/javascripts/app.js +0 -1
  91. data/test/cgi/assets/stylesheets/app.css +0 -1
  92. data/test/cgi/lighttpd.conf +0 -26
  93. data/test/cgi/rackup_stub.rb +0 -6
  94. data/test/cgi/sample_rackup.ru +0 -5
  95. data/test/cgi/test +0 -9
  96. data/test/cgi/test+directory/test+file +0 -1
  97. data/test/cgi/test.fcgi +0 -9
  98. data/test/cgi/test.gz +0 -0
  99. data/test/cgi/test.ru +0 -5
  100. data/test/gemloader.rb +0 -10
  101. data/test/helper.rb +0 -34
  102. data/test/multipart/bad_robots +0 -259
  103. data/test/multipart/binary +0 -0
  104. data/test/multipart/content_type_and_no_filename +0 -6
  105. data/test/multipart/empty +0 -10
  106. data/test/multipart/fail_16384_nofile +0 -814
  107. data/test/multipart/file1.txt +0 -1
  108. data/test/multipart/filename_and_modification_param +0 -7
  109. data/test/multipart/filename_and_no_name +0 -6
  110. data/test/multipart/filename_with_encoded_words +0 -7
  111. data/test/multipart/filename_with_escaped_quotes +0 -6
  112. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  113. data/test/multipart/filename_with_null_byte +0 -7
  114. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  115. data/test/multipart/filename_with_single_quote +0 -7
  116. data/test/multipart/filename_with_unescaped_percentages +0 -6
  117. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  118. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  119. data/test/multipart/filename_with_unescaped_quotes +0 -6
  120. data/test/multipart/ie +0 -6
  121. data/test/multipart/invalid_character +0 -6
  122. data/test/multipart/mixed_files +0 -21
  123. data/test/multipart/nested +0 -10
  124. data/test/multipart/none +0 -9
  125. data/test/multipart/quoted +0 -15
  126. data/test/multipart/rack-logo.png +0 -0
  127. data/test/multipart/semicolon +0 -6
  128. data/test/multipart/text +0 -15
  129. data/test/multipart/three_files_three_fields +0 -31
  130. data/test/multipart/unity3d_wwwform +0 -11
  131. data/test/multipart/webkit +0 -32
  132. data/test/rackup/config.ru +0 -31
  133. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  134. data/test/spec_auth_basic.rb +0 -89
  135. data/test/spec_auth_digest.rb +0 -260
  136. data/test/spec_body_proxy.rb +0 -85
  137. data/test/spec_builder.rb +0 -233
  138. data/test/spec_cascade.rb +0 -63
  139. data/test/spec_cgi.rb +0 -84
  140. data/test/spec_chunked.rb +0 -103
  141. data/test/spec_common_logger.rb +0 -107
  142. data/test/spec_conditional_get.rb +0 -103
  143. data/test/spec_config.rb +0 -23
  144. data/test/spec_content_length.rb +0 -86
  145. data/test/spec_content_type.rb +0 -46
  146. data/test/spec_deflater.rb +0 -375
  147. data/test/spec_directory.rb +0 -148
  148. data/test/spec_etag.rb +0 -108
  149. data/test/spec_events.rb +0 -133
  150. data/test/spec_fastcgi.rb +0 -85
  151. data/test/spec_file.rb +0 -264
  152. data/test/spec_handler.rb +0 -57
  153. data/test/spec_head.rb +0 -46
  154. data/test/spec_lint.rb +0 -520
  155. data/test/spec_lobster.rb +0 -59
  156. data/test/spec_lock.rb +0 -204
  157. data/test/spec_logger.rb +0 -24
  158. data/test/spec_media_type.rb +0 -42
  159. data/test/spec_method_override.rb +0 -110
  160. data/test/spec_mime.rb +0 -51
  161. data/test/spec_mock.rb +0 -359
  162. data/test/spec_multipart.rb +0 -721
  163. data/test/spec_null_logger.rb +0 -21
  164. data/test/spec_recursive.rb +0 -75
  165. data/test/spec_request.rb +0 -1423
  166. data/test/spec_response.rb +0 -528
  167. data/test/spec_rewindable_input.rb +0 -128
  168. data/test/spec_runtime.rb +0 -50
  169. data/test/spec_sendfile.rb +0 -125
  170. data/test/spec_server.rb +0 -193
  171. data/test/spec_session_abstract_id.rb +0 -31
  172. data/test/spec_session_abstract_session_hash.rb +0 -45
  173. data/test/spec_session_cookie.rb +0 -442
  174. data/test/spec_session_memcache.rb +0 -357
  175. data/test/spec_session_persisted_secure_secure_session_hash.rb +0 -73
  176. data/test/spec_session_pool.rb +0 -247
  177. data/test/spec_show_exceptions.rb +0 -93
  178. data/test/spec_show_status.rb +0 -104
  179. data/test/spec_static.rb +0 -184
  180. data/test/spec_tempfile_reaper.rb +0 -64
  181. data/test/spec_thin.rb +0 -96
  182. data/test/spec_urlmap.rb +0 -237
  183. data/test/spec_utils.rb +0 -742
  184. data/test/spec_version.rb +0 -11
  185. data/test/spec_webrick.rb +0 -206
  186. data/test/static/another/index.html +0 -1
  187. data/test/static/foo.html +0 -1
  188. data/test/static/index.html +0 -1
  189. data/test/testrequest.rb +0 -78
  190. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  191. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/builder.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Rack::Builder implements a small DSL to iteratively construct Rack
3
5
  # applications.
@@ -29,32 +31,102 @@ module Rack
29
31
  # You can use +map+ to construct a Rack::URLMap in a convenient way.
30
32
 
31
33
  class Builder
34
+
35
+ # https://stackoverflow.com/questions/2223882/whats-the-difference-between-utf-8-and-utf-8-without-bom
36
+ UTF_8_BOM = '\xef\xbb\xbf'
37
+
38
+ # Parse the given config file to get a Rack application.
39
+ #
40
+ # If the config file ends in +.ru+, it is treated as a
41
+ # rackup file and the contents will be treated as if
42
+ # specified inside a Rack::Builder block, using the given
43
+ # options.
44
+ #
45
+ # If the config file does not end in +.ru+, it is
46
+ # required and Rack will use the basename of the file
47
+ # to guess which constant will be the Rack application to run.
48
+ # The options given will be ignored in this case.
49
+ #
50
+ # Examples:
51
+ #
52
+ # Rack::Builder.parse_file('config.ru')
53
+ # # Rack application built using Rack::Builder.new
54
+ #
55
+ # Rack::Builder.parse_file('app.rb')
56
+ # # requires app.rb, which can be anywhere in Ruby's
57
+ # # load path. After requiring, assumes App constant
58
+ # # contains Rack application
59
+ #
60
+ # Rack::Builder.parse_file('./my_app.rb')
61
+ # # requires ./my_app.rb, which should be in the
62
+ # # process's current directory. After requiring,
63
+ # # assumes MyApp constant contains Rack application
32
64
  def self.parse_file(config, opts = Server::Options.new)
33
- options = {}
34
- if config =~ /\.ru$/
35
- cfgfile = ::File.read(config)
36
- if cfgfile[/^#\\(.*)/] && opts
37
- options = opts.parse! $1.split(/\s+/)
38
- end
39
- cfgfile.sub!(/^__END__\n.*\Z/m, '')
40
- app = new_from_string cfgfile, config
65
+ if config.end_with?('.ru')
66
+ return self.load_file(config, opts)
41
67
  else
42
68
  require config
43
69
  app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join(''))
70
+ return app, {}
71
+ end
72
+ end
73
+
74
+ # Load the given file as a rackup file, treating the
75
+ # contents as if specified inside a Rack::Builder block.
76
+ #
77
+ # Treats the first comment at the beginning of a line
78
+ # that starts with a backslash as options similar to
79
+ # options passed on a rackup command line.
80
+ #
81
+ # Ignores content in the file after +__END__+, so that
82
+ # use of +__END__+ will not result in a syntax error.
83
+ #
84
+ # Example config.ru file:
85
+ #
86
+ # $ cat config.ru
87
+ #
88
+ # #\ -p 9393
89
+ #
90
+ # use Rack::ContentLength
91
+ # require './app.rb'
92
+ # run App
93
+ def self.load_file(path, opts = Server::Options.new)
94
+ options = {}
95
+
96
+ cfgfile = ::File.read(path)
97
+ cfgfile.slice!(/\A#{UTF_8_BOM}/) if cfgfile.encoding == Encoding::UTF_8
98
+
99
+ if cfgfile[/^#\\(.*)/] && opts
100
+ warn "Parsing options from the first comment line is deprecated!"
101
+ options = opts.parse! $1.split(/\s+/)
44
102
  end
103
+
104
+ cfgfile.sub!(/^__END__\n.*\Z/m, '')
105
+ app = new_from_string cfgfile, path
106
+
45
107
  return app, options
46
108
  end
47
109
 
48
- def self.new_from_string(builder_script, file="(rackup)")
49
- eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
50
- TOPLEVEL_BINDING, file, 0
110
+ # Evaluate the given +builder_script+ string in the context of
111
+ # a Rack::Builder block, returning a Rack application.
112
+ def self.new_from_string(builder_script, file = "(rackup)")
113
+ # We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.
114
+ # We cannot use instance_eval(String) as that would resolve constants differently.
115
+ binding, builder = TOPLEVEL_BINDING.eval('Rack::Builder.new.instance_eval { [binding, self] }')
116
+ eval builder_script, binding, file
117
+ builder.to_app
51
118
  end
52
119
 
120
+ # Initialize a new Rack::Builder instance. +default_app+ specifies the
121
+ # default application if +run+ is not called later. If a block
122
+ # is given, it is evaluted in the context of the instance.
53
123
  def initialize(default_app = nil, &block)
54
- @use, @map, @run, @warmup = [], nil, default_app, nil
124
+ @use, @map, @run, @warmup, @freeze_app = [], nil, default_app, nil, false
55
125
  instance_eval(&block) if block_given?
56
126
  end
57
127
 
128
+ # Create a new Rack::Builder instance and return the Rack application
129
+ # generated from it.
58
130
  def self.app(default_app = nil, &block)
59
131
  self.new(default_app, &block).to_app
60
132
  end
@@ -81,10 +153,11 @@ module Rack
81
153
  def use(middleware, *args, &block)
82
154
  if @map
83
155
  mapping, @map = @map, nil
84
- @use << proc { |app| generate_map app, mapping }
156
+ @use << proc { |app| generate_map(app, mapping) }
85
157
  end
86
158
  @use << proc { |app| middleware.new(app, *args, &block) }
87
159
  end
160
+ ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
88
161
 
89
162
  # Takes an argument that is an object that responds to #call and returns a Rack response.
90
163
  # The simplest form of this is a lambda object:
@@ -104,7 +177,8 @@ module Rack
104
177
  @run = app
105
178
  end
106
179
 
107
- # Takes a lambda or block that is used to warm-up the application.
180
+ # Takes a lambda or block that is used to warm-up the application. This block is called
181
+ # before the Rack application is returned by to_app.
108
182
  #
109
183
  # warmup do |app|
110
184
  # client = Rack::MockRequest.new(app)
@@ -113,51 +187,70 @@ module Rack
113
187
  #
114
188
  # use SomeMiddleware
115
189
  # run MyApp
116
- def warmup(prc=nil, &block)
190
+ def warmup(prc = nil, &block)
117
191
  @warmup = prc || block
118
192
  end
119
193
 
120
- # Creates a route within the application.
194
+ # Creates a route within the application. Routes under the mapped path will be sent to
195
+ # the Rack application specified by run inside the block. Other requests will be sent to the
196
+ # default application specified by run outside the block.
121
197
  #
122
198
  # Rack::Builder.app do
123
- # map '/' do
199
+ # map '/heartbeat' do
124
200
  # run Heartbeat
125
201
  # end
202
+ # run App
126
203
  # end
127
204
  #
128
- # The +use+ method can also be used here to specify middleware to run under a specific path:
205
+ # The +use+ method can also be used inside the block to specify middleware to run under a specific path:
129
206
  #
130
207
  # Rack::Builder.app do
131
- # map '/' do
208
+ # map '/heartbeat' do
132
209
  # use Middleware
133
210
  # run Heartbeat
134
211
  # end
212
+ # run App
135
213
  # end
136
214
  #
137
- # This example includes a piece of middleware which will run before requests hit +Heartbeat+.
215
+ # This example includes a piece of middleware which will run before +/heartbeat+ requests hit +Heartbeat+.
138
216
  #
217
+ # Note that providing a +path+ of +/+ will ignore any default application given in a +run+ statement
218
+ # outside the block.
139
219
  def map(path, &block)
140
220
  @map ||= {}
141
221
  @map[path] = block
142
222
  end
143
223
 
224
+ # Freeze the app (set using run) and all middleware instances when building the application
225
+ # in to_app.
226
+ def freeze_app
227
+ @freeze_app = true
228
+ end
229
+
230
+ # Return the Rack application generated by this instance.
144
231
  def to_app
145
232
  app = @map ? generate_map(@run, @map) : @run
146
233
  fail "missing run or map statement" unless app
147
- app = @use.reverse.inject(app) { |a,e| e[a] }
234
+ app.freeze if @freeze_app
235
+ app = @use.reverse.inject(app) { |a, e| e[a].tap { |x| x.freeze if @freeze_app } }
148
236
  @warmup.call(app) if @warmup
149
237
  app
150
238
  end
151
239
 
240
+ # Call the Rack application generated by this builder instance. Note that
241
+ # this rebuilds the Rack application and runs the warmup code (if any)
242
+ # every time it is called, so it should not be used if performance is important.
152
243
  def call(env)
153
244
  to_app.call(env)
154
245
  end
155
246
 
156
247
  private
157
248
 
249
+ # Generate a URLMap instance by generating new Rack applications for each
250
+ # map block in this instance.
158
251
  def generate_map(default_app, mapping)
159
- mapped = default_app ? {'/' => default_app} : {}
160
- mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app }
252
+ mapped = default_app ? { '/' => default_app } : {}
253
+ mapping.each { |r, b| mapped[r] = self.class.new(default_app, &b).to_app }
161
254
  URLMap.new(mapped)
162
255
  end
163
256
  end
data/lib/rack/cascade.rb CHANGED
@@ -1,24 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Rack::Cascade tries a request on several apps, and returns the
3
- # first response that is not 404 or 405 (or in a list of configurable
4
- # status codes).
5
+ # first response that is not 404 or 405 (or in a list of configured
6
+ # status codes). If all applications tried return one of the configured
7
+ # status codes, return the last response.
5
8
 
6
9
  class Cascade
7
- NotFound = [404, {CONTENT_TYPE => "text/plain"}, []]
10
+ # deprecated, no longer used
11
+ NotFound = [404, { CONTENT_TYPE => "text/plain" }, []]
8
12
 
13
+ # An array of applications to try in order.
9
14
  attr_reader :apps
10
15
 
11
- def initialize(apps, catch=[404, 405])
12
- @apps = []; @has_app = {}
16
+ # Set the apps to send requests to, and what statuses result in
17
+ # cascading. Arguments:
18
+ #
19
+ # apps: An enumerable of rack applications.
20
+ # cascade_for: The statuses to use cascading for. If a response is received
21
+ # from an app, the next app is tried.
22
+ def initialize(apps, cascade_for = [404, 405])
23
+ @apps = []
13
24
  apps.each { |app| add app }
14
25
 
15
- @catch = {}
16
- [*catch].each { |status| @catch[status] = true }
26
+ @cascade_for = {}
27
+ [*cascade_for].each { |status| @cascade_for[status] = true }
17
28
  end
18
29
 
30
+ # Call each app in order. If the responses uses a status that requires
31
+ # cascading, try the next app. If all responses require cascading,
32
+ # return the response from the last app.
19
33
  def call(env)
20
- result = NotFound
21
-
34
+ return [404, { CONTENT_TYPE => "text/plain" }, []] if @apps.empty?
35
+ result = nil
22
36
  last_body = nil
23
37
 
24
38
  @apps.each do |app|
@@ -31,20 +45,22 @@ module Rack
31
45
  last_body.close if last_body.respond_to? :close
32
46
 
33
47
  result = app.call(env)
48
+ return result unless @cascade_for.include?(result[0].to_i)
34
49
  last_body = result[2]
35
- break unless @catch.include?(result[0].to_i)
36
50
  end
37
51
 
38
52
  result
39
53
  end
40
54
 
55
+ # Append an app to the list of apps to cascade. This app will
56
+ # be tried last.
41
57
  def add(app)
42
- @has_app[app] = true
43
58
  @apps << app
44
59
  end
45
60
 
61
+ # Whether the given app is one of the apps to cascade to.
46
62
  def include?(app)
47
- @has_app.include? app
63
+ @apps.include?(app)
48
64
  end
49
65
 
50
66
  alias_method :<<, :add
data/lib/rack/chunked.rb CHANGED
@@ -1,69 +1,117 @@
1
- require 'rack/utils'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Rack
4
4
 
5
5
  # Middleware that applies chunked transfer encoding to response bodies
6
6
  # when the response does not include a Content-Length header.
7
+ #
8
+ # This supports the Trailer response header to allow the use of trailing
9
+ # headers in the chunked encoding. However, using this requires you manually
10
+ # specify a response body that supports a +trailers+ method. Example:
11
+ #
12
+ # [200, { 'Trailer' => 'Expires'}, ["Hello", "World"]]
13
+ # # error raised
14
+ #
15
+ # body = ["Hello", "World"]
16
+ # def body.trailers
17
+ # { 'Expires' => Time.now.to_s }
18
+ # end
19
+ # [200, { 'Trailer' => 'Expires'}, body]
20
+ # # No exception raised
7
21
  class Chunked
8
22
  include Rack::Utils
9
23
 
10
- # A body wrapper that emits chunked responses
24
+ # A body wrapper that emits chunked responses.
11
25
  class Body
12
26
  TERM = "\r\n"
13
- TAIL = "0#{TERM}#{TERM}"
14
-
15
- include Rack::Utils
27
+ TAIL = "0#{TERM}"
16
28
 
29
+ # Store the response body to be chunked.
17
30
  def initialize(body)
18
31
  @body = body
19
32
  end
20
33
 
21
- def each
34
+ # For each element yielded by the response body, yield
35
+ # the element in chunked encoding.
36
+ def each(&block)
22
37
  term = TERM
23
38
  @body.each do |chunk|
24
39
  size = chunk.bytesize
25
40
  next if size == 0
26
41
 
27
- chunk = chunk.b
28
- yield [size.to_s(16), term, chunk, term].join
42
+ yield [size.to_s(16), term, chunk.b, term].join
29
43
  end
30
44
  yield TAIL
45
+ yield_trailers(&block)
46
+ yield term
31
47
  end
32
48
 
49
+ # Close the response body if the response body supports it.
33
50
  def close
34
51
  @body.close if @body.respond_to?(:close)
35
52
  end
53
+
54
+ private
55
+
56
+ # Do nothing as this class does not support trailer headers.
57
+ def yield_trailers
58
+ end
59
+ end
60
+
61
+ # A body wrapper that emits chunked responses and also supports
62
+ # sending Trailer headers. Note that the response body provided to
63
+ # initialize must have a +trailers+ method that returns a hash
64
+ # of trailer headers, and the rack response itself should have a
65
+ # Trailer header listing the headers that the +trailers+ method
66
+ # will return.
67
+ class TrailerBody < Body
68
+ private
69
+
70
+ # Yield strings for each trailer header.
71
+ def yield_trailers
72
+ @body.trailers.each_pair do |k, v|
73
+ yield "#{k}: #{v}\r\n"
74
+ end
75
+ end
36
76
  end
37
77
 
38
78
  def initialize(app)
39
79
  @app = app
40
80
  end
41
81
 
42
- # pre-HTTP/1.0 (informally "HTTP/0.9") HTTP requests did not have
43
- # a version (nor response headers)
82
+ # Whether the HTTP version supports chunked encoding (HTTP 1.1 does).
44
83
  def chunkable_version?(ver)
45
84
  case ver
46
- when "HTTP/1.0", nil, "HTTP/0.9"
85
+ # pre-HTTP/1.0 (informally "HTTP/0.9") HTTP requests did not have
86
+ # a version (nor response headers)
87
+ when 'HTTP/1.0', nil, 'HTTP/0.9'
47
88
  false
48
89
  else
49
90
  true
50
91
  end
51
92
  end
52
93
 
94
+ # If the rack app returns a response that should have a body,
95
+ # but does not have Content-Length or Transfer-Encoding headers,
96
+ # modify the response to use chunked Transfer-Encoding.
53
97
  def call(env)
54
98
  status, headers, body = @app.call(env)
55
- headers = HeaderHash.new(headers)
99
+ headers = HeaderHash[headers]
100
+
101
+ if chunkable_version?(env[SERVER_PROTOCOL]) &&
102
+ !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
103
+ !headers[CONTENT_LENGTH] &&
104
+ !headers[TRANSFER_ENCODING]
56
105
 
57
- if ! chunkable_version?(env[HTTP_VERSION]) ||
58
- STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
59
- headers[CONTENT_LENGTH] ||
60
- headers[TRANSFER_ENCODING]
61
- [status, headers, body]
62
- else
63
- headers.delete(CONTENT_LENGTH)
64
106
  headers[TRANSFER_ENCODING] = 'chunked'
65
- [status, headers, Body.new(body)]
107
+ if headers['Trailer']
108
+ body = TrailerBody.new(body)
109
+ else
110
+ body = Body.new(body)
111
+ end
66
112
  end
113
+
114
+ [status, headers, body]
67
115
  end
68
116
  end
69
117
  end
@@ -1,63 +1,66 @@
1
- require 'rack/body_proxy'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Rack
4
4
  # Rack::CommonLogger forwards every request to the given +app+, and
5
5
  # logs a line in the
6
6
  # {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common]
7
- # to the +logger+.
8
- #
9
- # If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is
10
- # an instance of Rack::NullLogger.
11
- #
12
- # +logger+ can be any class, including the standard library Logger, and is
13
- # expected to have either +write+ or +<<+ method, which accepts the CommonLogger::FORMAT.
14
- # According to the SPEC, the error stream must also respond to +puts+
15
- # (which takes a single argument that responds to +to_s+), and +flush+
16
- # (which is called without arguments in order to make the error appear for
17
- # sure)
7
+ # to the configured logger.
18
8
  class CommonLogger
19
9
  # Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
20
10
  #
21
11
  # lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 -
22
12
  #
23
13
  # %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
24
- FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n}
14
+ #
15
+ # The actual format is slightly different than the above due to the
16
+ # separation of SCRIPT_NAME and PATH_INFO, and because the elapsed
17
+ # time in seconds is included at the end.
18
+ FORMAT = %{%s - %s [%s] "%s %s%s%s %s" %d %s %0.4f\n}
25
19
 
26
- def initialize(app, logger=nil)
20
+ # +logger+ can be any object that supports the +write+ or +<<+ methods,
21
+ # which includes the standard library Logger. These methods are called
22
+ # with a single string argument, the log message.
23
+ # If +logger+ is nil, CommonLogger will fall back <tt>env['rack.errors']</tt>.
24
+ def initialize(app, logger = nil)
27
25
  @app = app
28
26
  @logger = logger
29
27
  end
30
28
 
29
+ # Log all requests in common_log format after a response has been
30
+ # returned. Note that if the app raises an exception, the request
31
+ # will not be logged, so if exception handling middleware are used,
32
+ # they should be loaded after this middleware. Additionally, because
33
+ # the logging happens after the request body has been fully sent, any
34
+ # exceptions raised during the sending of the response body will
35
+ # cause the request not to be logged.
31
36
  def call(env)
32
- began_at = Time.now
33
- status, header, body = @app.call(env)
34
- header = Utils::HeaderHash.new(header)
35
- body = BodyProxy.new(body) { log(env, status, header, began_at) }
36
- [status, header, body]
37
+ began_at = Utils.clock_time
38
+ status, headers, body = @app.call(env)
39
+ headers = Utils::HeaderHash[headers]
40
+ body = BodyProxy.new(body) { log(env, status, headers, began_at) }
41
+ [status, headers, body]
37
42
  end
38
43
 
39
44
  private
40
45
 
46
+ # Log the request to the configured logger.
41
47
  def log(env, status, header, began_at)
42
- now = Time.now
43
48
  length = extract_content_length(header)
44
49
 
45
50
  msg = FORMAT % [
46
51
  env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
47
52
  env["REMOTE_USER"] || "-",
48
- now.strftime("%d/%b/%Y:%H:%M:%S %z"),
53
+ Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
49
54
  env[REQUEST_METHOD],
55
+ env[SCRIPT_NAME],
50
56
  env[PATH_INFO],
51
57
  env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
52
- env[HTTP_VERSION],
58
+ env[SERVER_PROTOCOL],
53
59
  status.to_s[0..3],
54
60
  length,
55
- now - began_at ]
56
-
57
- msg.gsub!(/[^[:print:]\n]/) { |c| "\\x#{c.ord}" }
61
+ Utils.clock_time - began_at ]
58
62
 
59
63
  logger = @logger || env[RACK_ERRORS]
60
-
61
64
  # Standard library logger doesn't support write but it supports << which actually
62
65
  # calls to write on the log device without formatting
63
66
  if logger.respond_to?(:write)
@@ -67,9 +70,11 @@ module Rack
67
70
  end
68
71
  end
69
72
 
73
+ # Attempt to determine the content length for the response to
74
+ # include it in the logged data.
70
75
  def extract_content_length(headers)
71
- value = headers[CONTENT_LENGTH] or return '-'
72
- value.to_s == '0' ? '-' : value
76
+ value = headers[CONTENT_LENGTH]
77
+ !value || value.to_s == '0' ? '-' : value
73
78
  end
74
79
  end
75
80
  end
@@ -1,4 +1,4 @@
1
- require 'rack/utils'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Rack
4
4
 
@@ -19,11 +19,13 @@ module Rack
19
19
  @app = app
20
20
  end
21
21
 
22
+ # Return empty 304 response if the response has not been
23
+ # modified since the last request.
22
24
  def call(env)
23
25
  case env[REQUEST_METHOD]
24
26
  when "GET", "HEAD"
25
27
  status, headers, body = @app.call(env)
26
- headers = Utils::HeaderHash.new(headers)
28
+ headers = Utils::HeaderHash[headers]
27
29
  if status == 200 && fresh?(env, headers)
28
30
  status = 304
29
31
  headers.delete(CONTENT_TYPE)
@@ -41,38 +43,40 @@ module Rack
41
43
 
42
44
  private
43
45
 
46
+ # Return whether the response has not been modified since the
47
+ # last request.
44
48
  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
49
+ # If-None-Match has priority over If-Modified-Since per RFC 7232
50
+ if none_match = env['HTTP_IF_NONE_MATCH']
51
+ etag_matches?(none_match, headers)
52
+ elsif (modified_since = env['HTTP_IF_MODIFIED_SINCE']) && (modified_since = to_rfc2822(modified_since))
53
+ modified_since?(modified_since, headers)
54
+ end
54
55
  end
55
56
 
57
+ # Whether the ETag response header matches the If-None-Match request header.
58
+ # If so, the request has not been modified.
56
59
  def etag_matches?(none_match, headers)
57
- etag = headers['ETag'] and etag == none_match
60
+ headers['ETag'] == none_match
58
61
  end
59
62
 
63
+ # Whether the Last-Modified response header matches the If-Modified-Since
64
+ # request header. If so, the request has not been modified.
60
65
  def modified_since?(modified_since, headers)
61
66
  last_modified = to_rfc2822(headers['Last-Modified']) and
62
- modified_since and
63
67
  modified_since >= last_modified
64
68
  end
65
69
 
70
+ # Return a Time object for the given string (which should be in RFC2822
71
+ # format), or nil if the string cannot be parsed.
66
72
  def to_rfc2822(since)
67
73
  # shortest possible valid date is the obsolete: 1 Nov 97 09:55 A
68
74
  # anything shorter is invalid, this avoids exceptions for common cases
69
75
  # most common being the empty string
70
76
  if since && since.length >= 16
71
- # NOTE: there is no trivial way to write this in a non execption way
77
+ # NOTE: there is no trivial way to write this in a non exception way
72
78
  # _rfc2822 returns a hash but is not that usable
73
79
  Time.rfc2822(since) rescue nil
74
- else
75
- nil
76
80
  end
77
81
  end
78
82
  end
data/lib/rack/config.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Rack::Config modifies the environment using the block given during
3
5
  # initialization.
@@ -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,12 +15,11 @@ 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
@@ -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