rack 2.0.9.3 → 2.2.0

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 (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/request.rb CHANGED
@@ -1,5 +1,4 @@
1
- require 'rack/utils'
2
- require 'rack/media_type'
1
+ # frozen_string_literal: true
3
2
 
4
3
  module Rack
5
4
  # Rack::Request provides a convenient interface to a Rack
@@ -11,7 +10,18 @@ module Rack
11
10
  # req.params["data"]
12
11
 
13
12
  class Request
14
- SCHEME_WHITELIST = %w(https http).freeze
13
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
14
+
15
+ class << self
16
+ attr_accessor :ip_filter
17
+ end
18
+
19
+ self.ip_filter = lambda { |ip| /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i.match?(ip) }
20
+ ALLOWED_SCHEMES = %w(https http).freeze
21
+ SCHEME_WHITELIST = ALLOWED_SCHEMES
22
+ if Object.respond_to?(:deprecate_constant)
23
+ deprecate_constant :SCHEME_WHITELIST
24
+ end
15
25
 
16
26
  def initialize(env)
17
27
  @params = nil
@@ -78,7 +88,7 @@ module Rack
78
88
  # assert_equal 'image/png,*/*', request.get_header('Accept')
79
89
  #
80
90
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
81
- def add_header key, v
91
+ def add_header(key, v)
82
92
  if v.nil?
83
93
  get_header key
84
94
  elsif has_header? key
@@ -100,7 +110,7 @@ module Rack
100
110
 
101
111
  module Helpers
102
112
  # The set of form-data media-types. Requests that do not indicate
103
- # one of the media types presents in this list will not be eligible
113
+ # one of the media types present in this list will not be eligible
104
114
  # for form-data / param parsing.
105
115
  FORM_DATA_MEDIA_TYPES = [
106
116
  'application/x-www-form-urlencoded',
@@ -108,7 +118,7 @@ module Rack
108
118
  ]
109
119
 
110
120
  # The set of media-types. Requests that do not indicate
111
- # one of the media types presents in this list will not be eligible
121
+ # one of the media types present in this list will not be eligible
112
122
  # for param parsing like soap attachments or generic multiparts
113
123
  PARSEABLE_DATA_MEDIA_TYPES = [
114
124
  'multipart/related',
@@ -119,11 +129,23 @@ module Rack
119
129
  # to include the port in a generated URI.
120
130
  DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
121
131
 
122
- HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'.freeze
123
- HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'.freeze
124
- HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'.freeze
125
- HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'.freeze
126
- HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'.freeze
132
+ # The address of the client which connected to the proxy.
133
+ HTTP_X_FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR'
134
+
135
+ # The contents of the host/:authority header sent to the proxy.
136
+ HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
137
+
138
+ # The value of the scheme sent to the proxy.
139
+ HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
140
+
141
+ # The protocol used to connect to the proxy.
142
+ HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
143
+
144
+ # The port used to connect to the proxy.
145
+ HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
146
+
147
+ # Another way for specifing https scheme was used.
148
+ HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
127
149
 
128
150
  def body; get_header(RACK_INPUT) end
129
151
  def script_name; get_header(SCRIPT_NAME).to_s end
@@ -159,10 +181,10 @@ module Rack
159
181
  def delete?; request_method == DELETE end
160
182
 
161
183
  # Checks the HTTP request method (or verb) to see if it was of type GET
162
- def get?; request_method == GET end
184
+ def get?; request_method == GET end
163
185
 
164
186
  # Checks the HTTP request method (or verb) to see if it was of type HEAD
165
- def head?; request_method == HEAD end
187
+ def head?; request_method == HEAD end
166
188
 
167
189
  # Checks the HTTP request method (or verb) to see if it was of type OPTIONS
168
190
  def options?; request_method == OPTIONS end
@@ -197,19 +219,52 @@ module Rack
197
219
  end
198
220
  end
199
221
 
222
+ # The authority of the incoming request as defined by RFC3976.
223
+ # https://tools.ietf.org/html/rfc3986#section-3.2
224
+ #
225
+ # In HTTP/1, this is the `host` header.
226
+ # In HTTP/2, this is the `:authority` pseudo-header.
200
227
  def authority
201
- get_header(SERVER_NAME) + ':' + get_header(SERVER_PORT)
228
+ forwarded_authority || host_authority || server_authority
229
+ end
230
+
231
+ # The authority as defined by the `SERVER_NAME` and `SERVER_PORT`
232
+ # variables.
233
+ def server_authority
234
+ host = self.server_name
235
+ port = self.server_port
236
+
237
+ if host
238
+ if port
239
+ "#{host}:#{port}"
240
+ else
241
+ host
242
+ end
243
+ end
244
+ end
245
+
246
+ def server_name
247
+ get_header(SERVER_NAME)
248
+ end
249
+
250
+ def server_port
251
+ if port = get_header(SERVER_PORT)
252
+ Integer(port)
253
+ end
202
254
  end
203
255
 
204
256
  def cookies
205
- hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
206
- set_header(k, {})
257
+ hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |key|
258
+ set_header(key, {})
259
+ end
260
+
261
+ string = get_header(HTTP_COOKIE)
262
+
263
+ unless string == get_header(RACK_REQUEST_COOKIE_STRING)
264
+ hash.replace Utils.parse_cookies_header(string)
265
+ set_header(RACK_REQUEST_COOKIE_STRING, string)
207
266
  end
208
- string = get_header HTTP_COOKIE
209
267
 
210
- return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
211
- hash.replace Utils.parse_cookies_header get_header HTTP_COOKIE
212
- set_header(RACK_REQUEST_COOKIE_STRING, string)
213
268
  hash
214
269
  end
215
270
 
@@ -222,46 +277,91 @@ module Rack
222
277
  get_header("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
223
278
  end
224
279
 
225
- def host_with_port
226
- if forwarded = get_header(HTTP_X_FORWARDED_HOST)
227
- forwarded.split(/,\s?/).last
280
+ # The `HTTP_HOST` header.
281
+ def host_authority
282
+ get_header(HTTP_HOST)
283
+ end
284
+
285
+ def host_with_port(authority = self.authority)
286
+ host, _, port = split_authority(authority)
287
+
288
+ if port == DEFAULT_PORTS[self.scheme]
289
+ host
228
290
  else
229
- get_header(HTTP_HOST) || "#{get_header(SERVER_NAME) || get_header(SERVER_ADDR)}:#{get_header(SERVER_PORT)}"
291
+ authority
230
292
  end
231
293
  end
232
294
 
295
+ # Returns a formatted host, suitable for being used in a URI.
233
296
  def host
234
- # Remove port number.
235
- host_with_port.to_s.sub(/:\d+\z/, '')
297
+ split_authority(self.authority)[0]
298
+ end
299
+
300
+ # Returns an address suitable for being to resolve to an address.
301
+ # In the case of a domain name or IPv4 address, the result is the same
302
+ # as +host+. In the case of IPv6 or future address formats, the square
303
+ # brackets are removed.
304
+ def hostname
305
+ split_authority(self.authority)[1]
236
306
  end
237
307
 
238
308
  def port
239
- if port = host_with_port.split(/:/)[1]
240
- port.to_i
241
- elsif port = get_header(HTTP_X_FORWARDED_PORT)
242
- port.to_i
243
- elsif has_header?(HTTP_X_FORWARDED_HOST)
244
- DEFAULT_PORTS[scheme]
245
- elsif has_header?(HTTP_X_FORWARDED_PROTO)
246
- DEFAULT_PORTS[get_header(HTTP_X_FORWARDED_PROTO).split(',')[0]]
247
- else
248
- get_header(SERVER_PORT).to_i
309
+ if authority = self.authority
310
+ _, _, port = split_authority(self.authority)
311
+
312
+ if port
313
+ return port
314
+ end
315
+ end
316
+
317
+ if forwarded_port = self.forwarded_port
318
+ return forwarded_port.first
319
+ end
320
+
321
+ if scheme = self.scheme
322
+ if port = DEFAULT_PORTS[self.scheme]
323
+ return port
324
+ end
325
+ end
326
+
327
+ self.server_port
328
+ end
329
+
330
+ def forwarded_for
331
+ if value = get_header(HTTP_X_FORWARDED_FOR)
332
+ split_header(value).map do |authority|
333
+ split_authority(wrap_ipv6(authority))[1]
334
+ end
335
+ end
336
+ end
337
+
338
+ def forwarded_port
339
+ if value = get_header(HTTP_X_FORWARDED_PORT)
340
+ split_header(value).map(&:to_i)
341
+ end
342
+ end
343
+
344
+ def forwarded_authority
345
+ if value = get_header(HTTP_X_FORWARDED_HOST)
346
+ wrap_ipv6(split_header(value).first)
249
347
  end
250
348
  end
251
349
 
252
350
  def ssl?
253
- scheme == 'https'
351
+ scheme == 'https' || scheme == 'wss'
254
352
  end
255
353
 
256
354
  def ip
257
- remote_addrs = split_ip_addresses(get_header('REMOTE_ADDR'))
355
+ remote_addrs = split_header(get_header('REMOTE_ADDR'))
258
356
  remote_addrs = reject_trusted_ip_addresses(remote_addrs)
259
357
 
260
- return remote_addrs.first if remote_addrs.any?
261
-
262
- forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))
358
+ if remote_addrs.any?
359
+ remote_addrs.first
360
+ else
361
+ forwarded_ips = self.forwarded_for
263
362
 
264
- return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
363
+ reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
364
+ end
265
365
  end
266
366
 
267
367
  # The media type (type/subtype) portion of the CONTENT_TYPE header
@@ -302,6 +402,7 @@ module Rack
302
402
  def form_data?
303
403
  type = media_type
304
404
  meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD)
405
+
305
406
  (meth == POST && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
306
407
  end
307
408
 
@@ -337,7 +438,7 @@ module Rack
337
438
 
338
439
  # Fix for Safari Ajax postings that always append \0
339
440
  # form_vars.sub!(/\0\z/, '') # performance replacement:
340
- form_vars.slice!(-1) if form_vars[-1] == ?\0
441
+ form_vars.slice!(-1) if form_vars.end_with?("\0")
341
442
 
342
443
  set_header RACK_REQUEST_FORM_VARS, form_vars
343
444
  set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
@@ -356,8 +457,6 @@ module Rack
356
457
  # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
357
458
  def params
358
459
  self.GET.merge(self.POST)
359
- rescue EOFError
360
- self.GET.dup
361
460
  end
362
461
 
363
462
  # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
@@ -386,13 +485,12 @@ module Rack
386
485
  #
387
486
  # <tt>env['rack.input']</tt> is not touched.
388
487
  def delete_param(k)
389
- [ self.POST.delete(k), self.GET.delete(k) ].compact.first
488
+ post_value, get_value = self.POST.delete(k), self.GET.delete(k)
489
+ post_value || get_value
390
490
  end
391
491
 
392
492
  def base_url
393
- url = "#{scheme}://#{host}"
394
- url << ":#{port}" if port != DEFAULT_PORTS[scheme]
395
- url
493
+ "#{scheme}://#{host_with_port}"
396
494
  end
397
495
 
398
496
  # Tries to return a remake of the original request URL as a string.
@@ -417,7 +515,7 @@ module Rack
417
515
  end
418
516
 
419
517
  def trusted_proxy?(ip)
420
- ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i
518
+ Rack::Request.ip_filter.call(ip)
421
519
  end
422
520
 
423
521
  # shortcut for <tt>request.params[key]</tt>
@@ -449,6 +547,20 @@ module Rack
449
547
 
450
548
  def default_session; {}; end
451
549
 
550
+ # Assist with compatibility when processing `X-Forwarded-For`.
551
+ def wrap_ipv6(host)
552
+ # Even thought IPv6 addresses should be wrapped in square brackets,
553
+ # sometimes this is not done in various legacy/underspecified headers.
554
+ # So we try to fix this situation for compatibility reasons.
555
+
556
+ # Try to detect IPv6 addresses which aren't escaped yet:
557
+ if !host.start_with?('[') && host.count(':') > 1
558
+ "[#{host}]"
559
+ else
560
+ host
561
+ end
562
+ end
563
+
452
564
  def parse_http_accept_header(header)
453
565
  header.to_s.split(/\s*,\s*/).map do |part|
454
566
  attribute, parameters = part.split(/\s*;\s*/, 2)
@@ -464,7 +576,7 @@ module Rack
464
576
  Utils.default_query_parser
465
577
  end
466
578
 
467
- def parse_query(qs, d='&')
579
+ def parse_query(qs, d = '&')
468
580
  query_parser.parse_nested_query(qs, d)
469
581
  end
470
582
 
@@ -472,8 +584,39 @@ module Rack
472
584
  Rack::Multipart.extract_multipart(self, query_parser)
473
585
  end
474
586
 
475
- def split_ip_addresses(ip_addresses)
476
- ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
587
+ def split_header(value)
588
+ value ? value.strip.split(/[,\s]+/) : []
589
+ end
590
+
591
+ AUTHORITY = /
592
+ # The host:
593
+ (?<host>
594
+ # An IPv6 address:
595
+ (\[(?<ip6>.*)\])
596
+ |
597
+ # An IPv4 address:
598
+ (?<ip4>[\d\.]+)
599
+ |
600
+ # A hostname:
601
+ (?<name>[a-zA-Z0-9\.\-]+)
602
+ )
603
+ # The optional port:
604
+ (:(?<port>\d+))?
605
+ /x
606
+
607
+ private_constant :AUTHORITY
608
+
609
+ def split_authority(authority)
610
+ if match = AUTHORITY.match(authority)
611
+ if address = match[:ip6]
612
+ return match[:host], address, match[:port]&.to_i
613
+ else
614
+ return match[:host], match[:host], match[:port]&.to_i
615
+ end
616
+ end
617
+
618
+ # Give up!
619
+ return authority, authority, nil
477
620
  end
478
621
 
479
622
  def reject_trusted_ip_addresses(ip_addresses)
@@ -481,16 +624,22 @@ module Rack
481
624
  end
482
625
 
483
626
  def forwarded_scheme
484
- scheme_headers = [
485
- get_header(HTTP_X_FORWARDED_SCHEME),
486
- get_header(HTTP_X_FORWARDED_PROTO).to_s.split(',')[0]
487
- ]
627
+ allowed_scheme(get_header(HTTP_X_FORWARDED_SCHEME)) ||
628
+ allowed_scheme(extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO)))
629
+ end
488
630
 
489
- scheme_headers.each do |header|
490
- return header if SCHEME_WHITELIST.include?(header)
491
- end
631
+ def allowed_scheme(header)
632
+ header if ALLOWED_SCHEMES.include?(header)
633
+ end
492
634
 
493
- nil
635
+ def extract_proto_header(header)
636
+ if header
637
+ if (comma_index = header.index(','))
638
+ header[0, comma_index]
639
+ else
640
+ header
641
+ end
642
+ end
494
643
  end
495
644
  end
496
645
 
data/lib/rack/response.rb CHANGED
@@ -1,7 +1,5 @@
1
- require 'rack/request'
2
- require 'rack/utils'
3
- require 'rack/body_proxy'
4
- require 'rack/media_type'
1
+ # frozen_string_literal: true
2
+
5
3
  require 'time'
6
4
 
7
5
  module Rack
@@ -17,38 +15,57 @@ module Rack
17
15
  # +write+ are synchronous with the Rack response.
18
16
  #
19
17
  # Your application's +call+ should end returning Response#finish.
20
-
21
18
  class Response
19
+ def self.[](status, headers, body)
20
+ self.new(body, status, headers)
21
+ end
22
+
23
+ CHUNKED = 'chunked'
24
+ STATUS_WITH_NO_ENTITY_BODY = Utils::STATUS_WITH_NO_ENTITY_BODY
25
+
22
26
  attr_accessor :length, :status, :body
23
- attr_reader :header
24
- alias headers header
27
+ attr_reader :headers
25
28
 
26
- CHUNKED = 'chunked'.freeze
29
+ # @deprecated Use {#headers} instead.
30
+ alias header headers
27
31
 
28
- def initialize(body=[], status=200, header={})
32
+ # Initialize the response object with the specified body, status
33
+ # and headers.
34
+ #
35
+ # @param body [nil, #each, #to_str] the response body.
36
+ # @param status [Integer] the integer status as defined by the
37
+ # HTTP protocol RFCs.
38
+ # @param headers [#each] a list of key-value header pairs which
39
+ # conform to the HTTP protocol RFCs.
40
+ #
41
+ # Providing a body which responds to #to_str is legacy behaviour.
42
+ def initialize(body = nil, status = 200, headers = {})
29
43
  @status = status.to_i
30
- @header = Utils::HeaderHash.new.merge(header)
44
+ @headers = Utils::HeaderHash[headers]
31
45
 
32
- @writer = lambda { |x| @body << x }
33
- @block = nil
34
- @length = 0
46
+ @writer = self.method(:append)
35
47
 
36
- @body = []
48
+ @block = nil
37
49
 
38
- if body.respond_to? :to_str
39
- write body.to_str
40
- elsif body.respond_to?(:each)
41
- body.each { |part|
42
- write part.to_s
43
- }
50
+ # Keep track of whether we have expanded the user supplied body.
51
+ if body.nil?
52
+ @body = []
53
+ @buffered = true
54
+ @length = 0
55
+ elsif body.respond_to?(:to_str)
56
+ @body = [body]
57
+ @buffered = true
58
+ @length = body.to_str.bytesize
44
59
  else
45
- raise TypeError, "stringable or iterable required"
60
+ @body = body
61
+ @buffered = false
62
+ @length = 0
46
63
  end
47
64
 
48
- yield self if block_given?
65
+ yield self if block_given?
49
66
  end
50
67
 
51
- def redirect(target, status=302)
68
+ def redirect(target, status = 302)
52
69
  self.status = status
53
70
  self.location = target
54
71
  end
@@ -57,42 +74,49 @@ module Rack
57
74
  CHUNKED == get_header(TRANSFER_ENCODING)
58
75
  end
59
76
 
77
+ # Generate a response array consistent with the requirements of the SPEC.
78
+ # @return [Array] a 3-tuple suitable of `[status, headers, body]`
79
+ # which is suitable to be returned from the middleware `#call(env)` method.
60
80
  def finish(&block)
61
- @block = block
62
-
63
- if [204, 304].include?(status.to_i)
81
+ if STATUS_WITH_NO_ENTITY_BODY[status.to_i]
64
82
  delete_header CONTENT_TYPE
65
83
  delete_header CONTENT_LENGTH
66
84
  close
67
- [status.to_i, header, []]
85
+ return [@status, @headers, []]
68
86
  else
69
- [status.to_i, header, BodyProxy.new(self){}]
87
+ if block_given?
88
+ @block = block
89
+ return [@status, @headers, self]
90
+ else
91
+ return [@status, @headers, @body]
92
+ end
70
93
  end
71
94
  end
95
+
72
96
  alias to_a finish # For *response
73
- alias to_ary finish # For implicit-splat on Ruby 1.9.2
74
97
 
75
98
  def each(&callback)
76
99
  @body.each(&callback)
77
- @writer = callback
78
- @block.call(self) if @block
100
+ @buffered = true
101
+
102
+ if @block
103
+ @writer = callback
104
+ @block.call(self)
105
+ end
79
106
  end
80
107
 
81
108
  # Append to body and update Content-Length.
82
109
  #
83
110
  # NOTE: Do not mix #write and direct #body access!
84
111
  #
85
- def write(str)
86
- s = str.to_s
87
- @length += s.bytesize unless chunked?
88
- @writer.call s
112
+ def write(chunk)
113
+ buffered_body!
89
114
 
90
- set_header(CONTENT_LENGTH, @length.to_s) unless chunked?
91
- str
115
+ @writer.call(chunk.to_s)
92
116
  end
93
117
 
94
118
  def close
95
- body.close if body.respond_to?(:close)
119
+ @body.close if @body.respond_to?(:close)
96
120
  end
97
121
 
98
122
  def empty?
@@ -144,7 +168,7 @@ module Rack
144
168
  # assert_equal 'Accept-Encoding,Cookie', response.get_header('Vary')
145
169
  #
146
170
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
147
- def add_header key, v
171
+ def add_header(key, v)
148
172
  if v.nil?
149
173
  get_header key
150
174
  elsif has_header? key
@@ -154,10 +178,16 @@ module Rack
154
178
  end
155
179
  end
156
180
 
181
+ # Get the content type of the response.
157
182
  def content_type
158
183
  get_header CONTENT_TYPE
159
184
  end
160
185
 
186
+ # Set the content type of the response.
187
+ def content_type=(content_type)
188
+ set_header CONTENT_TYPE, content_type
189
+ end
190
+
161
191
  def media_type
162
192
  MediaType.type(content_type)
163
193
  end
@@ -184,7 +214,7 @@ module Rack
184
214
  set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value)
185
215
  end
186
216
 
187
- def delete_cookie(key, value={})
217
+ def delete_cookie(key, value = {})
188
218
  set_header SET_COOKIE, ::Rack::Utils.add_remove_cookie_to_header(get_header(SET_COOKIE), key, value)
189
219
  end
190
220
 
@@ -192,7 +222,7 @@ module Rack
192
222
  get_header SET_COOKIE
193
223
  end
194
224
 
195
- def set_cookie_header= v
225
+ def set_cookie_header=(v)
196
226
  set_header SET_COOKIE, v
197
227
  end
198
228
 
@@ -200,17 +230,70 @@ module Rack
200
230
  get_header CACHE_CONTROL
201
231
  end
202
232
 
203
- def cache_control= v
233
+ def cache_control=(v)
204
234
  set_header CACHE_CONTROL, v
205
235
  end
206
236
 
237
+ # Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
238
+ def do_not_cache!
239
+ set_header CACHE_CONTROL, "no-cache, must-revalidate"
240
+ set_header EXPIRES, Time.now.httpdate
241
+ end
242
+
243
+ # Specify that the content should be cached.
244
+ # @param duration [Integer] The number of seconds until the cache expires.
245
+ # @option directive [String] The cache control directive, one of "public", "private", "no-cache" or "no-store".
246
+ def cache!(duration = 3600, directive: "public")
247
+ unless headers[CACHE_CONTROL] =~ /no-cache/
248
+ set_header CACHE_CONTROL, "#{directive}, max-age=#{duration}"
249
+ set_header EXPIRES, (Time.now + duration).httpdate
250
+ end
251
+ end
252
+
207
253
  def etag
208
254
  get_header ETAG
209
255
  end
210
256
 
211
- def etag= v
257
+ def etag=(v)
212
258
  set_header ETAG, v
213
259
  end
260
+
261
+ protected
262
+
263
+ def buffered_body!
264
+ return if @buffered
265
+
266
+ if @body.is_a?(Array)
267
+ # The user supplied body was an array:
268
+ @body = @body.compact
269
+ @body.each do |part|
270
+ @length += part.to_s.bytesize
271
+ end
272
+ else
273
+ # Turn the user supplied body into a buffered array:
274
+ body = @body
275
+ @body = Array.new
276
+
277
+ body.each do |part|
278
+ @writer.call(part.to_s)
279
+ end
280
+
281
+ body.close if body.respond_to?(:close)
282
+ end
283
+
284
+ @buffered = true
285
+ end
286
+
287
+ def append(chunk)
288
+ @body << chunk
289
+
290
+ unless chunked?
291
+ @length += chunk.bytesize
292
+ set_header(CONTENT_LENGTH, @length.to_s)
293
+ end
294
+
295
+ return chunk
296
+ end
214
297
  end
215
298
 
216
299
  include Helpers
@@ -221,7 +304,7 @@ module Rack
221
304
  attr_reader :headers
222
305
  attr_accessor :status
223
306
 
224
- def initialize status, headers
307
+ def initialize(status, headers)
225
308
  @status = status
226
309
  @headers = headers
227
310
  end