rack 2.2.4 → 3.0.8

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.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +223 -71
  3. data/CONTRIBUTING.md +53 -47
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +309 -0
  6. data/SPEC.rdoc +183 -131
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +3 -1
  9. data/lib/rack/auth/basic.rb +0 -2
  10. data/lib/rack/auth/digest/md5.rb +1 -131
  11. data/lib/rack/auth/digest/nonce.rb +1 -54
  12. data/lib/rack/auth/digest/params.rb +1 -54
  13. data/lib/rack/auth/digest/request.rb +1 -43
  14. data/lib/rack/auth/digest.rb +256 -0
  15. data/lib/rack/body_proxy.rb +3 -1
  16. data/lib/rack/builder.rb +83 -63
  17. data/lib/rack/cascade.rb +2 -0
  18. data/lib/rack/chunked.rb +16 -13
  19. data/lib/rack/common_logger.rb +23 -18
  20. data/lib/rack/conditional_get.rb +18 -15
  21. data/lib/rack/constants.rb +64 -0
  22. data/lib/rack/content_length.rb +12 -16
  23. data/lib/rack/content_type.rb +8 -5
  24. data/lib/rack/deflater.rb +40 -26
  25. data/lib/rack/directory.rb +9 -3
  26. data/lib/rack/etag.rb +14 -23
  27. data/lib/rack/events.rb +4 -0
  28. data/lib/rack/file.rb +2 -0
  29. data/lib/rack/files.rb +15 -17
  30. data/lib/rack/head.rb +9 -8
  31. data/lib/rack/headers.rb +154 -0
  32. data/lib/rack/lint.rb +783 -682
  33. data/lib/rack/lock.rb +2 -5
  34. data/lib/rack/logger.rb +2 -0
  35. data/lib/rack/media_type.rb +1 -1
  36. data/lib/rack/method_override.rb +6 -2
  37. data/lib/rack/mime.rb +8 -0
  38. data/lib/rack/mock.rb +1 -271
  39. data/lib/rack/mock_request.rb +166 -0
  40. data/lib/rack/mock_response.rb +126 -0
  41. data/lib/rack/multipart/generator.rb +7 -5
  42. data/lib/rack/multipart/parser.rb +134 -65
  43. data/lib/rack/multipart/uploaded_file.rb +4 -0
  44. data/lib/rack/multipart.rb +20 -40
  45. data/lib/rack/null_logger.rb +9 -0
  46. data/lib/rack/query_parser.rb +78 -46
  47. data/lib/rack/recursive.rb +2 -0
  48. data/lib/rack/reloader.rb +0 -2
  49. data/lib/rack/request.rb +226 -108
  50. data/lib/rack/response.rb +136 -61
  51. data/lib/rack/rewindable_input.rb +24 -5
  52. data/lib/rack/runtime.rb +7 -6
  53. data/lib/rack/sendfile.rb +30 -25
  54. data/lib/rack/show_exceptions.rb +15 -2
  55. data/lib/rack/show_status.rb +17 -7
  56. data/lib/rack/static.rb +8 -8
  57. data/lib/rack/tempfile_reaper.rb +15 -4
  58. data/lib/rack/urlmap.rb +4 -2
  59. data/lib/rack/utils.rb +223 -185
  60. data/lib/rack/version.rb +9 -4
  61. data/lib/rack.rb +6 -76
  62. metadata +18 -38
  63. data/README.rdoc +0 -306
  64. data/Rakefile +0 -130
  65. data/bin/rackup +0 -5
  66. data/contrib/rack.png +0 -0
  67. data/contrib/rack.svg +0 -150
  68. data/contrib/rack_logo.svg +0 -164
  69. data/contrib/rdoc.css +0 -412
  70. data/example/lobster.ru +0 -6
  71. data/example/protectedlobster.rb +0 -16
  72. data/example/protectedlobster.ru +0 -10
  73. data/lib/rack/core_ext/regexp.rb +0 -14
  74. data/lib/rack/handler/cgi.rb +0 -59
  75. data/lib/rack/handler/fastcgi.rb +0 -100
  76. data/lib/rack/handler/lsws.rb +0 -61
  77. data/lib/rack/handler/scgi.rb +0 -71
  78. data/lib/rack/handler/thin.rb +0 -36
  79. data/lib/rack/handler/webrick.rb +0 -129
  80. data/lib/rack/handler.rb +0 -104
  81. data/lib/rack/lobster.rb +0 -70
  82. data/lib/rack/server.rb +0 -466
  83. data/lib/rack/session/abstract/id.rb +0 -523
  84. data/lib/rack/session/cookie.rb +0 -203
  85. data/lib/rack/session/memcache.rb +0 -10
  86. data/lib/rack/session/pool.rb +0 -85
  87. data/rack.gemspec +0 -46
data/lib/rack/static.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'constants'
4
+ require_relative 'files'
5
+ require_relative 'mime'
6
+
3
7
  module Rack
4
8
 
5
9
  # The Rack::Static middleware intercepts requests for static files
@@ -78,16 +82,14 @@ module Rack
78
82
  # :header_rules => [
79
83
  # # Cache all static files in public caches (e.g. Rack::Cache)
80
84
  # # as well as in the browser
81
- # [:all, {'Cache-Control' => 'public, max-age=31536000'}],
85
+ # [:all, {'cache-control' => 'public, max-age=31536000'}],
82
86
  #
83
87
  # # Provide web fonts with cross-origin access-control-headers
84
88
  # # Firefox requires this when serving assets using a Content Delivery Network
85
- # [:fonts, {'Access-Control-Allow-Origin' => '*'}]
89
+ # [:fonts, {'access-control-allow-origin' => '*'}]
86
90
  # ]
87
91
  #
88
92
  class Static
89
- (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
90
-
91
93
  def initialize(app, options = {})
92
94
  @app = app
93
95
  @urls = options[:urls] || ["/favicon.ico"]
@@ -137,10 +139,8 @@ module Rack
137
139
  elsif response[0] == 304
138
140
  # Do nothing, leave headers as is
139
141
  else
140
- if mime_type = Mime.mime_type(::File.extname(path), 'text/plain')
141
- response[1][CONTENT_TYPE] = mime_type
142
- end
143
- response[1]['Content-Encoding'] = 'gzip'
142
+ response[1][CONTENT_TYPE] = Mime.mime_type(::File.extname(path), 'text/plain')
143
+ response[1]['content-encoding'] = 'gzip'
144
144
  end
145
145
  end
146
146
 
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'constants'
4
+ require_relative 'body_proxy'
5
+
3
6
  module Rack
4
7
 
5
8
  # Middleware tracks and cleans Tempfiles created throughout a request (i.e. Rack::Multipart)
@@ -12,11 +15,19 @@ module Rack
12
15
 
13
16
  def call(env)
14
17
  env[RACK_TEMPFILES] ||= []
15
- status, headers, body = @app.call(env)
16
- body_proxy = BodyProxy.new(body) do
17
- env[RACK_TEMPFILES].each(&:close!) unless env[RACK_TEMPFILES].nil?
18
+
19
+ begin
20
+ _, _, body = response = @app.call(env)
21
+ rescue Exception
22
+ env[RACK_TEMPFILES]&.each(&:close!)
23
+ raise
18
24
  end
19
- [status, headers, body_proxy]
25
+
26
+ response[2] = BodyProxy.new(body) do
27
+ env[RACK_TEMPFILES]&.each(&:close!)
28
+ end
29
+
30
+ response
20
31
  end
21
32
  end
22
33
  end
data/lib/rack/urlmap.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'set'
4
4
 
5
+ require_relative 'constants'
6
+
5
7
  module Rack
6
8
  # Rack::URLMap takes a hash mapping urls or paths to apps, and
7
9
  # dispatches accordingly. Support for HTTP/1.1 host names exists if
@@ -35,7 +37,7 @@ module Rack
35
37
  end
36
38
 
37
39
  location = location.chomp('/')
38
- match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
40
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", Regexp::NOENCODING)
39
41
 
40
42
  [host, location, match, app]
41
43
  }.sort_by do |(host, location, _, _)|
@@ -74,7 +76,7 @@ module Rack
74
76
  return app.call(env)
75
77
  end
76
78
 
77
- [404, { CONTENT_TYPE => "text/plain", "X-Cascade" => "pass" }, ["Not Found: #{path}"]]
79
+ [404, { CONTENT_TYPE => "text/plain", "x-cascade" => "pass" }, ["Not Found: #{path}"]]
78
80
 
79
81
  ensure
80
82
  env[PATH_INFO] = path
data/lib/rack/utils.rb CHANGED
@@ -8,29 +8,29 @@ require 'tempfile'
8
8
  require 'time'
9
9
 
10
10
  require_relative 'query_parser'
11
+ require_relative 'mime'
12
+ require_relative 'headers'
13
+ require_relative 'constants'
11
14
 
12
15
  module Rack
13
16
  # Rack::Utils contains a grab-bag of useful methods for writing web
14
17
  # applications adopted from all kinds of Ruby libraries.
15
18
 
16
19
  module Utils
17
- (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
18
-
19
20
  ParameterTypeError = QueryParser::ParameterTypeError
20
21
  InvalidParameterError = QueryParser::InvalidParameterError
22
+ ParamsTooDeepError = QueryParser::ParamsTooDeepError
21
23
  DEFAULT_SEP = QueryParser::DEFAULT_SEP
22
24
  COMMON_SEP = QueryParser::COMMON_SEP
23
25
  KeySpaceConstrainedParams = QueryParser::Params
24
26
 
25
- RFC2822_DAY_NAME = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
26
- RFC2822_MONTH_NAME = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
27
-
28
27
  class << self
29
28
  attr_accessor :default_query_parser
30
29
  end
31
- # The default number of bytes to allow parameter keys to take up.
32
- # This helps prevent a rogue client from flooding a Request.
33
- self.default_query_parser = QueryParser.make_default(65536, 100)
30
+ # The default amount of nesting to allowed by hash parameters.
31
+ # This helps prevent a rogue client from triggering a possible stack overflow
32
+ # when parsing parameters.
33
+ self.default_query_parser = QueryParser.make_default(32)
34
34
 
35
35
  module_function
36
36
 
@@ -58,13 +58,24 @@ module Rack
58
58
  end
59
59
 
60
60
  class << self
61
- attr_accessor :multipart_part_limit
61
+ attr_accessor :multipart_total_part_limit
62
+
63
+ attr_accessor :multipart_file_limit
64
+
65
+ # multipart_part_limit is the original name of multipart_file_limit, but
66
+ # the limit only counts parts with filenames.
67
+ alias multipart_part_limit multipart_file_limit
68
+ alias multipart_part_limit= multipart_file_limit=
62
69
  end
63
70
 
64
- # The maximum number of parts a request can contain. Accepting too many part
65
- # can lead to the server running out of file handles.
71
+ # The maximum number of file parts a request can contain. Accepting too
72
+ # many parts can lead to the server running out of file handles.
66
73
  # Set to `0` for no limit.
67
- self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
74
+ self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
75
+
76
+ # The maximum total number of parts a request can contain. Accepting too
77
+ # many can lead to excessive memory use and parsing time.
78
+ self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
68
79
 
69
80
  def self.param_depth_limit
70
81
  default_query_parser.param_depth_limit
@@ -75,11 +86,12 @@ module Rack
75
86
  end
76
87
 
77
88
  def self.key_space_limit
78
- default_query_parser.key_space_limit
89
+ warn("`Rack::Utils.key_space_limit` is deprecated as this value no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
90
+ 65536
79
91
  end
80
92
 
81
93
  def self.key_space_limit=(v)
82
- self.default_query_parser = self.default_query_parser.new_space_limit(v)
94
+ warn("`Rack::Utils.key_space_limit=` is deprecated and no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
83
95
  end
84
96
 
85
97
  if defined?(Process::CLOCK_MONOTONIC)
@@ -120,13 +132,13 @@ module Rack
120
132
  }.join("&")
121
133
  when Hash
122
134
  value.map { |k, v|
123
- build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
135
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
124
136
  }.delete_if(&:empty?).join('&')
125
137
  when nil
126
- prefix
138
+ escape(prefix)
127
139
  else
128
140
  raise ArgumentError, "value must be a Hash" if prefix.nil?
129
- "#{prefix}=#{escape(value)}"
141
+ "#{escape(prefix)}=#{escape(value)}"
130
142
  end
131
143
  end
132
144
 
@@ -141,6 +153,19 @@ module Rack
141
153
  end
142
154
  end
143
155
 
156
+ def forwarded_values(forwarded_header)
157
+ return nil unless forwarded_header
158
+ forwarded_header = forwarded_header.to_s.gsub("\n", ";")
159
+
160
+ forwarded_header.split(/\s*;\s*/).each_with_object({}) do |field, values|
161
+ field.split(/\s*,\s*/).each do |pair|
162
+ return nil unless pair =~ /\A\s*(by|for|host|proto)\s*=\s*"?([^"]+)"?\s*\Z/i
163
+ (values[$1.downcase.to_sym] ||= []) << $2
164
+ end
165
+ end
166
+ end
167
+ module_function :forwarded_values
168
+
144
169
  # Return best accept value to use, based on the algorithm
145
170
  # in RFC 2616 Section 14. If there are multiple best
146
171
  # matches (same specificity and quality), the value returned
@@ -155,7 +180,7 @@ module Rack
155
180
  end.compact.sort_by do |match, quality|
156
181
  (match.split('/', 2).count('*') * -10) + quality
157
182
  end.last
158
- matches && matches.first
183
+ matches&.first
159
184
  end
160
185
 
161
186
  ESCAPE_HTML = {
@@ -206,17 +231,20 @@ module Rack
206
231
  (encoding_candidates & available_encodings)[0]
207
232
  end
208
233
 
209
- def parse_cookies(env)
210
- parse_cookies_header env[HTTP_COOKIE]
211
- end
234
+ # :call-seq:
235
+ # parse_cookies_header(value) -> hash
236
+ #
237
+ # Parse cookies from the provided header +value+ according to RFC6265. The
238
+ # syntax for cookie headers only supports semicolons. Returns a map of
239
+ # cookie +key+ to cookie +value+.
240
+ #
241
+ # parse_cookies_header('myname=myvalue; max-age=0')
242
+ # # => {"myname"=>"myvalue", "max-age"=>"0"}
243
+ #
244
+ def parse_cookies_header(value)
245
+ return {} unless value
212
246
 
213
- def parse_cookies_header(header)
214
- # According to RFC 6265:
215
- # The syntax for cookie headers only supports semicolons
216
- # User Agent -> Server ==
217
- # Cookie: SID=31d4d96e407aad42; lang=en-US
218
- return {} unless header
219
- header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
247
+ value.split(/; */n).each_with_object({}) do |cookie, cookies|
220
248
  next if cookie.empty?
221
249
  key, value = cookie.split('=', 2)
222
250
  cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
@@ -224,14 +252,66 @@ module Rack
224
252
  end
225
253
 
226
254
  def add_cookie_to_header(header, key, value)
255
+ warn("add_cookie_to_header is deprecated and will be removed in Rack 3.1", uplevel: 1)
256
+
257
+ case header
258
+ when nil, ''
259
+ return set_cookie_header(key, value)
260
+ when String
261
+ [header, set_cookie_header(key, value)]
262
+ when Array
263
+ header + [set_cookie_header(key, value)]
264
+ else
265
+ raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
266
+ end
267
+ end
268
+
269
+ # :call-seq:
270
+ # parse_cookies(env) -> hash
271
+ #
272
+ # Parse cookies from the provided request environment using
273
+ # parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
274
+ #
275
+ # parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
276
+ # # => {'myname' => 'myvalue'}
277
+ #
278
+ def parse_cookies(env)
279
+ parse_cookies_header env[HTTP_COOKIE]
280
+ end
281
+
282
+ # :call-seq:
283
+ # set_cookie_header(key, value) -> encoded string
284
+ #
285
+ # Generate an encoded string using the provided +key+ and +value+ suitable
286
+ # for the +set-cookie+ header according to RFC6265. The +value+ may be an
287
+ # instance of either +String+ or +Hash+.
288
+ #
289
+ # If the cookie +value+ is an instance of +Hash+, it considers the following
290
+ # cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
291
+ # of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
292
+ # details about the interpretation of these fields, consult
293
+ # [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
294
+ #
295
+ # An extra cookie attribute +escape_key+ can be provided to control whether
296
+ # or not the cookie key is URL encoded. If explicitly set to +false+, the
297
+ # cookie key name will not be url encoded (escaped). The default is +true+.
298
+ #
299
+ # set_cookie_header("myname", "myvalue")
300
+ # # => "myname=myvalue"
301
+ #
302
+ # set_cookie_header("myname", {value: "myvalue", max_age: 10})
303
+ # # => "myname=myvalue; max-age=10"
304
+ #
305
+ def set_cookie_header(key, value)
227
306
  case value
228
307
  when Hash
308
+ key = escape(key) unless value[:escape_key] == false
229
309
  domain = "; domain=#{value[:domain]}" if value[:domain]
230
310
  path = "; path=#{value[:path]}" if value[:path]
231
311
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
232
312
  expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
233
313
  secure = "; secure" if value[:secure]
234
- httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
314
+ httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
235
315
  same_site =
236
316
  case value[:same_site]
237
317
  when false, nil
@@ -246,100 +326,109 @@ module Rack
246
326
  raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
247
327
  end
248
328
  value = value[:value]
329
+ else
330
+ key = escape(key)
249
331
  end
332
+
250
333
  value = [value] unless Array === value
251
334
 
252
- cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
335
+ return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
253
336
  "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
337
+ end
254
338
 
255
- case header
256
- when nil, ''
257
- cookie
258
- when String
259
- [header, cookie].join("\n")
260
- when Array
261
- (header + [cookie]).join("\n")
339
+ # :call-seq:
340
+ # set_cookie_header!(headers, key, value) -> header value
341
+ #
342
+ # Append a cookie in the specified headers with the given cookie +key+ and
343
+ # +value+ using set_cookie_header.
344
+ #
345
+ # If the headers already contains a +set-cookie+ key, it will be converted
346
+ # to an +Array+ if not already, and appended to.
347
+ def set_cookie_header!(headers, key, value)
348
+ if header = headers[SET_COOKIE]
349
+ if header.is_a?(Array)
350
+ header << set_cookie_header(key, value)
351
+ else
352
+ headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
353
+ end
262
354
  else
263
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
355
+ headers[SET_COOKIE] = set_cookie_header(key, value)
264
356
  end
265
357
  end
266
358
 
267
- def set_cookie_header!(header, key, value)
268
- header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
269
- nil
359
+ # :call-seq:
360
+ # delete_set_cookie_header(key, value = {}) -> encoded string
361
+ #
362
+ # Generate an encoded string based on the given +key+ and +value+ using
363
+ # set_cookie_header for the purpose of causing the specified cookie to be
364
+ # deleted. The +value+ may be an instance of +Hash+ and can include
365
+ # attributes as outlined by set_cookie_header. The encoded cookie will have
366
+ # a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
367
+ # +value+. When used with the +set-cookie+ header, it will cause the client
368
+ # to *remove* any matching cookie.
369
+ #
370
+ # delete_set_cookie_header("myname")
371
+ # # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
372
+ #
373
+ def delete_set_cookie_header(key, value = {})
374
+ set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
270
375
  end
271
376
 
272
377
  def make_delete_cookie_header(header, key, value)
273
- case header
274
- when nil, ''
275
- cookies = []
276
- when String
277
- cookies = header.split("\n")
278
- when Array
279
- cookies = header
280
- end
378
+ warn("make_delete_cookie_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
281
379
 
282
- key = escape(key)
283
- domain = value[:domain]
284
- path = value[:path]
285
- regexp = if domain
286
- if path
287
- /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
288
- else
289
- /\A#{key}=.*domain=#{domain}(?:;|$)/
290
- end
291
- elsif path
292
- /\A#{key}=.*path=#{path}(?:;|$)/
293
- else
294
- /\A#{key}=/
295
- end
296
-
297
- cookies.reject! { |cookie| regexp.match? cookie }
298
-
299
- cookies.join("\n")
380
+ delete_set_cookie_header!(header, key, value)
300
381
  end
301
382
 
302
- def delete_cookie_header!(header, key, value = {})
303
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
304
- nil
383
+ def delete_cookie_header!(headers, key, value = {})
384
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
385
+
386
+ return nil
305
387
  end
306
388
 
307
- # Adds a cookie that will *remove* a cookie from the client. Hence the
308
- # strange method name.
309
389
  def add_remove_cookie_to_header(header, key, value = {})
310
- new_header = make_delete_cookie_header(header, key, value)
390
+ warn("add_remove_cookie_to_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
311
391
 
312
- add_cookie_to_header(new_header, key,
313
- { value: '', path: nil, domain: nil,
314
- max_age: '0',
315
- expires: Time.at(0) }.merge(value))
392
+ delete_set_cookie_header!(header, key, value)
393
+ end
394
+
395
+ # :call-seq:
396
+ # delete_set_cookie_header!(header, key, value = {}) -> header value
397
+ #
398
+ # Set an expired cookie in the specified headers with the given cookie
399
+ # +key+ and +value+ using delete_set_cookie_header. This causes
400
+ # the client to immediately delete the specified cookie.
401
+ #
402
+ # delete_set_cookie_header!(nil, "mycookie")
403
+ # # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
404
+ #
405
+ # If the header is non-nil, it will be modified in place.
406
+ #
407
+ # header = []
408
+ # delete_set_cookie_header!(header, "mycookie")
409
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
410
+ # header
411
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
412
+ #
413
+ def delete_set_cookie_header!(header, key, value = {})
414
+ if header
415
+ header = Array(header)
416
+ header << delete_set_cookie_header(key, value)
417
+ else
418
+ header = delete_set_cookie_header(key, value)
419
+ end
316
420
 
421
+ return header
317
422
  end
318
423
 
319
424
  def rfc2822(time)
320
425
  time.rfc2822
321
426
  end
322
427
 
323
- # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
324
- # of '% %b %Y'.
325
- # It assumes that the time is in GMT to comply to the RFC 2109.
326
- #
327
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
328
- # that I'm certain someone implemented only that option.
329
- # Do not use %a and %b from Time.strptime, it would use localized names for
330
- # weekday and month.
331
- #
332
- def rfc2109(time)
333
- wday = RFC2822_DAY_NAME[time.wday]
334
- mon = RFC2822_MONTH_NAME[time.mon - 1]
335
- time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
336
- end
337
-
338
428
  # Parses the "Range:" header, if present, into an array of Range objects.
339
429
  # Returns nil if the header is missing or syntactically invalid.
340
430
  # Returns an empty array if none of the ranges are satisfiable.
341
431
  def byte_ranges(env, size)
342
- warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
343
432
  get_byte_ranges env['HTTP_RANGE'], size
344
433
  end
345
434
 
@@ -348,17 +437,18 @@ module Rack
348
437
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
349
438
  ranges = []
350
439
  $1.split(/,\s*/).each do |range_spec|
351
- return nil unless range_spec =~ /(\d*)-(\d*)/
352
- r0, r1 = $1, $2
353
- if r0.empty?
354
- return nil if r1.empty?
440
+ return nil unless range_spec.include?('-')
441
+ range = range_spec.split('-')
442
+ r0, r1 = range[0], range[1]
443
+ if r0.nil? || r0.empty?
444
+ return nil if r1.nil?
355
445
  # suffix-byte-range-spec, represents trailing suffix of file
356
446
  r0 = size - r1.to_i
357
447
  r0 = 0 if r0 < 0
358
448
  r1 = size - 1
359
449
  else
360
450
  r0 = r0.to_i
361
- if r1.empty?
451
+ if r1.nil?
362
452
  r1 = size - 1
363
453
  else
364
454
  r1 = r1.to_i
@@ -371,20 +461,30 @@ module Rack
371
461
  ranges
372
462
  end
373
463
 
374
- # Constant time string comparison.
375
- #
376
- # NOTE: the values compared should be of fixed length, such as strings
377
- # that have already been processed by HMAC. This should not be used
378
- # on variable length plaintext strings because it could leak length info
379
- # via timing attacks.
380
- def secure_compare(a, b)
381
- return false unless a.bytesize == b.bytesize
464
+ # :nocov:
465
+ if defined?(OpenSSL.fixed_length_secure_compare)
466
+ # Constant time string comparison.
467
+ #
468
+ # NOTE: the values compared should be of fixed length, such as strings
469
+ # that have already been processed by HMAC. This should not be used
470
+ # on variable length plaintext strings because it could leak length info
471
+ # via timing attacks.
472
+ def secure_compare(a, b)
473
+ return false unless a.bytesize == b.bytesize
474
+
475
+ OpenSSL.fixed_length_secure_compare(a, b)
476
+ end
477
+ # :nocov:
478
+ else
479
+ def secure_compare(a, b)
480
+ return false unless a.bytesize == b.bytesize
382
481
 
383
- l = a.unpack("C*")
482
+ l = a.unpack("C*")
384
483
 
385
- r, i = 0, -1
386
- b.each_byte { |v| r |= v ^ l[i += 1] }
387
- r == 0
484
+ r, i = 0, -1
485
+ b.each_byte { |v| r |= v ^ l[i += 1] }
486
+ r == 0
487
+ end
388
488
  end
389
489
 
390
490
  # Context allows the use of a compatible middleware at different points
@@ -413,94 +513,32 @@ module Rack
413
513
  end
414
514
  end
415
515
 
416
- # A case-insensitive Hash that preserves the original case of a
516
+ # A wrapper around Headers
417
517
  # header when set.
418
518
  #
419
519
  # @api private
420
520
  class HeaderHash < Hash # :nodoc:
421
521
  def self.[](headers)
422
- if headers.is_a?(HeaderHash) && !headers.frozen?
522
+ warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
523
+ if headers.is_a?(Headers) && !headers.frozen?
423
524
  return headers
424
- else
425
- return self.new(headers)
426
525
  end
427
- end
428
-
429
- def initialize(hash = {})
430
- super()
431
- @names = {}
432
- hash.each { |k, v| self[k] = v }
433
- end
434
-
435
- # on dup/clone, we need to duplicate @names hash
436
- def initialize_copy(other)
437
- super
438
- @names = other.names.dup
439
- end
440
-
441
- # on clear, we need to clear @names hash
442
- def clear
443
- super
444
- @names.clear
445
- end
446
-
447
- def each
448
- super do |k, v|
449
- yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
450
- end
451
- end
452
526
 
453
- def to_hash
454
- hash = {}
455
- each { |k, v| hash[k] = v }
456
- hash
527
+ new_headers = Headers.new
528
+ headers.each{|k,v| new_headers[k] = v}
529
+ new_headers
457
530
  end
458
531
 
459
- def [](k)
460
- super(k) || super(@names[k.downcase])
532
+ def self.new(hash = {})
533
+ warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
534
+ headers = Headers.new
535
+ hash.each{|k,v| headers[k] = v}
536
+ headers
461
537
  end
462
538
 
463
- def []=(k, v)
464
- canonical = k.downcase.freeze
465
- delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
466
- @names[canonical] = k
467
- super k, v
539
+ def self.allocate
540
+ raise TypeError, "cannot allocate HeaderHash"
468
541
  end
469
-
470
- def delete(k)
471
- canonical = k.downcase
472
- result = super @names.delete(canonical)
473
- result
474
- end
475
-
476
- def include?(k)
477
- super || @names.include?(k.downcase)
478
- end
479
-
480
- alias_method :has_key?, :include?
481
- alias_method :member?, :include?
482
- alias_method :key?, :include?
483
-
484
- def merge!(other)
485
- other.each { |k, v| self[k] = v }
486
- self
487
- end
488
-
489
- def merge(other)
490
- hash = dup
491
- hash.merge! other
492
- end
493
-
494
- def replace(other)
495
- clear
496
- other.each { |k, v| self[k] = v }
497
- self
498
- end
499
-
500
- protected
501
- def names
502
- @names
503
- end
504
542
  end
505
543
 
506
544
  # Every standard HTTP code mapped to the appropriate message.