rack 2.2.9 → 3.0.0.beta1

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -101
  3. data/CONTRIBUTING.md +53 -47
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +287 -0
  6. data/Rakefile +40 -7
  7. data/SPEC.rdoc +166 -125
  8. data/contrib/LICENSE.md +7 -0
  9. data/contrib/logo.webp +0 -0
  10. data/lib/rack/auth/abstract/handler.rb +3 -1
  11. data/lib/rack/auth/abstract/request.rb +3 -1
  12. data/lib/rack/auth/digest/md5.rb +1 -131
  13. data/lib/rack/auth/digest/nonce.rb +1 -54
  14. data/lib/rack/auth/digest/params.rb +1 -54
  15. data/lib/rack/auth/digest/request.rb +1 -43
  16. data/lib/rack/auth/digest.rb +256 -0
  17. data/lib/rack/body_proxy.rb +3 -1
  18. data/lib/rack/builder.rb +60 -42
  19. data/lib/rack/cascade.rb +2 -0
  20. data/lib/rack/chunked.rb +16 -13
  21. data/lib/rack/common_logger.rb +23 -18
  22. data/lib/rack/conditional_get.rb +18 -15
  23. data/lib/rack/constants.rb +62 -0
  24. data/lib/rack/content_length.rb +12 -16
  25. data/lib/rack/content_type.rb +8 -5
  26. data/lib/rack/deflater.rb +40 -26
  27. data/lib/rack/directory.rb +9 -3
  28. data/lib/rack/etag.rb +14 -23
  29. data/lib/rack/events.rb +4 -0
  30. data/lib/rack/file.rb +2 -0
  31. data/lib/rack/files.rb +15 -17
  32. data/lib/rack/head.rb +9 -8
  33. data/lib/rack/headers.rb +154 -0
  34. data/lib/rack/lint.rb +740 -649
  35. data/lib/rack/lock.rb +2 -5
  36. data/lib/rack/logger.rb +2 -0
  37. data/lib/rack/media_type.rb +4 -9
  38. data/lib/rack/method_override.rb +5 -1
  39. data/lib/rack/mime.rb +8 -0
  40. data/lib/rack/mock.rb +1 -271
  41. data/lib/rack/mock_request.rb +166 -0
  42. data/lib/rack/mock_response.rb +124 -0
  43. data/lib/rack/multipart/generator.rb +7 -5
  44. data/lib/rack/multipart/parser.rb +123 -85
  45. data/lib/rack/multipart/uploaded_file.rb +4 -0
  46. data/lib/rack/multipart.rb +20 -40
  47. data/lib/rack/null_logger.rb +9 -0
  48. data/lib/rack/query_parser.rb +76 -44
  49. data/lib/rack/recursive.rb +2 -0
  50. data/lib/rack/reloader.rb +0 -2
  51. data/lib/rack/request.rb +189 -91
  52. data/lib/rack/response.rb +131 -61
  53. data/lib/rack/rewindable_input.rb +24 -5
  54. data/lib/rack/runtime.rb +7 -6
  55. data/lib/rack/sendfile.rb +30 -25
  56. data/lib/rack/show_exceptions.rb +15 -2
  57. data/lib/rack/show_status.rb +17 -7
  58. data/lib/rack/static.rb +8 -8
  59. data/lib/rack/tempfile_reaper.rb +15 -4
  60. data/lib/rack/urlmap.rb +4 -2
  61. data/lib/rack/utils.rb +210 -199
  62. data/lib/rack/version.rb +9 -4
  63. data/lib/rack.rb +5 -76
  64. data/rack.gemspec +6 -6
  65. metadata +19 -31
  66. data/README.rdoc +0 -320
  67. data/bin/rackup +0 -5
  68. data/contrib/rack.png +0 -0
  69. data/contrib/rack.svg +0 -150
  70. data/contrib/rack_logo.svg +0 -164
  71. data/lib/rack/core_ext/regexp.rb +0 -14
  72. data/lib/rack/handler/cgi.rb +0 -59
  73. data/lib/rack/handler/fastcgi.rb +0 -100
  74. data/lib/rack/handler/lsws.rb +0 -61
  75. data/lib/rack/handler/scgi.rb +0 -71
  76. data/lib/rack/handler/thin.rb +0 -36
  77. data/lib/rack/handler/webrick.rb +0 -129
  78. data/lib/rack/handler.rb +0 -104
  79. data/lib/rack/lobster.rb +0 -70
  80. data/lib/rack/server.rb +0 -466
  81. data/lib/rack/session/abstract/id.rb +0 -523
  82. data/lib/rack/session/cookie.rb +0 -204
  83. data/lib/rack/session/memcache.rb +0 -10
  84. data/lib/rack/session/pool.rb +0 -85
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('/', '/+')}(.*)", Regexp::NOENCODING)
40
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
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,24 +58,13 @@ module Rack
58
58
  end
59
59
 
60
60
  class << self
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=
61
+ attr_accessor :multipart_part_limit
69
62
  end
70
63
 
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.
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.
73
66
  # Set to `0` for no limit.
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
67
+ self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
79
68
 
80
69
  def self.param_depth_limit
81
70
  default_query_parser.param_depth_limit
@@ -86,11 +75,12 @@ module Rack
86
75
  end
87
76
 
88
77
  def self.key_space_limit
89
- default_query_parser.key_space_limit
78
+ 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)
79
+ 65536
90
80
  end
91
81
 
92
82
  def self.key_space_limit=(v)
93
- self.default_query_parser = self.default_query_parser.new_space_limit(v)
83
+ warn("`Rack::Utils.key_space_limit=` is deprecated and no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
94
84
  end
95
85
 
96
86
  if defined?(Process::CLOCK_MONOTONIC)
@@ -142,8 +132,8 @@ module Rack
142
132
  end
143
133
 
144
134
  def q_values(q_value_header)
145
- q_value_header.to_s.split(',').map do |part|
146
- value, parameters = part.split(';', 2).map(&:strip)
135
+ q_value_header.to_s.split(/\s*,\s*/).map do |part|
136
+ value, parameters = part.split(/\s*;\s*/, 2)
147
137
  quality = 1.0
148
138
  if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
149
139
  quality = md[1].to_f
@@ -152,6 +142,19 @@ module Rack
152
142
  end
153
143
  end
154
144
 
145
+ def forwarded_values(forwarded_header)
146
+ return nil unless forwarded_header
147
+ forwarded_header = forwarded_header.to_s.gsub("\n", ";")
148
+
149
+ forwarded_header.split(/\s*;\s*/).each_with_object({}) do |field, values|
150
+ field.split(/\s*,\s*/).each do |pair|
151
+ return nil unless pair =~ /\A\s*(by|for|host|proto)\s*=\s*"?([^"]+)"?\s*\Z/i
152
+ (values[$1.downcase.to_sym] ||= []) << $2
153
+ end
154
+ end
155
+ end
156
+ module_function :forwarded_values
157
+
155
158
  # Return best accept value to use, based on the algorithm
156
159
  # in RFC 2616 Section 14. If there are multiple best
157
160
  # matches (same specificity and quality), the value returned
@@ -166,7 +169,7 @@ module Rack
166
169
  end.compact.sort_by do |match, quality|
167
170
  (match.split('/', 2).count('*') * -10) + quality
168
171
  end.last
169
- matches && matches.first
172
+ matches&.first
170
173
  end
171
174
 
172
175
  ESCAPE_HTML = {
@@ -217,17 +220,20 @@ module Rack
217
220
  (encoding_candidates & available_encodings)[0]
218
221
  end
219
222
 
220
- def parse_cookies(env)
221
- parse_cookies_header env[HTTP_COOKIE]
222
- end
223
+ # :call-seq:
224
+ # parse_cookies_header(value) -> hash
225
+ #
226
+ # Parse cookies from the provided header +value+ according to RFC6265. The
227
+ # syntax for cookie headers only supports semicolons. Returns a map of
228
+ # cookie +key+ to cookie +value+.
229
+ #
230
+ # parse_cookies_header('myname=myvalue; max-age=0')
231
+ # # => {"myname"=>"myvalue", "max-age"=>"0"}
232
+ #
233
+ def parse_cookies_header(value)
234
+ return {} unless value
223
235
 
224
- def parse_cookies_header(header)
225
- # According to RFC 6265:
226
- # The syntax for cookie headers only supports semicolons
227
- # User Agent -> Server ==
228
- # Cookie: SID=31d4d96e407aad42; lang=en-US
229
- return {} unless header
230
- header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
236
+ value.split(/; */n).each_with_object({}) do |cookie, cookies|
231
237
  next if cookie.empty?
232
238
  key, value = cookie.split('=', 2)
233
239
  cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
@@ -235,14 +241,66 @@ module Rack
235
241
  end
236
242
 
237
243
  def add_cookie_to_header(header, key, value)
244
+ warn("add_cookie_to_header is deprecated and will be removed in Rack 3.1", uplevel: 1)
245
+
246
+ case header
247
+ when nil, ''
248
+ return set_cookie_header(key, value)
249
+ when String
250
+ [header, set_cookie_header(key, value)]
251
+ when Array
252
+ header + [set_cookie_header(key, value)]
253
+ else
254
+ raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
255
+ end
256
+ end
257
+
258
+ # :call-seq:
259
+ # parse_cookies(env) -> hash
260
+ #
261
+ # Parse cookies from the provided request environment using
262
+ # parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
263
+ #
264
+ # parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
265
+ # # => {'myname' => 'myvalue'}
266
+ #
267
+ def parse_cookies(env)
268
+ parse_cookies_header env[HTTP_COOKIE]
269
+ end
270
+
271
+ # :call-seq:
272
+ # set_cookie_header(key, value) -> encoded string
273
+ #
274
+ # Generate an encoded string using the provided +key+ and +value+ suitable
275
+ # for the +set-cookie+ header according to RFC6265. The +value+ may be an
276
+ # instance of either +String+ or +Hash+.
277
+ #
278
+ # If the cookie +value+ is an instance of +Hash+, it considers the following
279
+ # cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
280
+ # of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
281
+ # details about the interpretation of these fields, consult
282
+ # [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
283
+ #
284
+ # An extra cookie attribute +escape_key+ can be provided to control whether
285
+ # or not the cookie key is URL encoded. If explicitly set to +false+, the
286
+ # cookie key name will not be url encoded (escaped). The default is +true+.
287
+ #
288
+ # set_cookie_header("myname", "myvalue")
289
+ # # => "myname=myvalue"
290
+ #
291
+ # set_cookie_header("myname", {value: "myvalue", max_age: 10})
292
+ # # => "myname=myvalue; max-age=10"
293
+ #
294
+ def set_cookie_header(key, value)
238
295
  case value
239
296
  when Hash
297
+ key = escape(key) unless value[:escape_key] == false
240
298
  domain = "; domain=#{value[:domain]}" if value[:domain]
241
299
  path = "; path=#{value[:path]}" if value[:path]
242
300
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
243
301
  expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
244
302
  secure = "; secure" if value[:secure]
245
- httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
303
+ httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
246
304
  same_site =
247
305
  case value[:same_site]
248
306
  when false, nil
@@ -257,100 +315,109 @@ module Rack
257
315
  raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
258
316
  end
259
317
  value = value[:value]
318
+ else
319
+ key = escape(key)
260
320
  end
321
+
261
322
  value = [value] unless Array === value
262
323
 
263
- cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
324
+ return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
264
325
  "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
326
+ end
265
327
 
266
- case header
267
- when nil, ''
268
- cookie
269
- when String
270
- [header, cookie].join("\n")
271
- when Array
272
- (header + [cookie]).join("\n")
328
+ # :call-seq:
329
+ # set_cookie_header!(headers, key, value) -> header value
330
+ #
331
+ # Append a cookie in the specified headers with the given cookie +key+ and
332
+ # +value+ using set_cookie_header.
333
+ #
334
+ # If the headers already contains a +set-cookie+ key, it will be converted
335
+ # to an +Array+ if not already, and appended to.
336
+ def set_cookie_header!(headers, key, value)
337
+ if header = headers[SET_COOKIE]
338
+ if header.is_a?(Array)
339
+ header << set_cookie_header(key, value)
340
+ else
341
+ headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
342
+ end
273
343
  else
274
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
344
+ headers[SET_COOKIE] = set_cookie_header(key, value)
275
345
  end
276
346
  end
277
347
 
278
- def set_cookie_header!(header, key, value)
279
- header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
280
- nil
348
+ # :call-seq:
349
+ # delete_set_cookie_header(key, value = {}) -> encoded string
350
+ #
351
+ # Generate an encoded string based on the given +key+ and +value+ using
352
+ # set_cookie_header for the purpose of causing the specified cookie to be
353
+ # deleted. The +value+ may be an instance of +Hash+ and can include
354
+ # attributes as outlined by set_cookie_header. The encoded cookie will have
355
+ # a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
356
+ # +value+. When used with the +set-cookie+ header, it will cause the client
357
+ # to *remove* any matching cookie.
358
+ #
359
+ # delete_set_cookie_header("myname")
360
+ # # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
361
+ #
362
+ def delete_set_cookie_header(key, value = {})
363
+ set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
281
364
  end
282
365
 
283
366
  def make_delete_cookie_header(header, key, value)
284
- case header
285
- when nil, ''
286
- cookies = []
287
- when String
288
- cookies = header.split("\n")
289
- when Array
290
- cookies = header
291
- end
292
-
293
- key = escape(key)
294
- domain = value[:domain]
295
- path = value[:path]
296
- regexp = if domain
297
- if path
298
- /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
299
- else
300
- /\A#{key}=.*domain=#{domain}(?:;|$)/
301
- end
302
- elsif path
303
- /\A#{key}=.*path=#{path}(?:;|$)/
304
- else
305
- /\A#{key}=/
306
- end
367
+ warn("make_delete_cookie_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
307
368
 
308
- cookies.reject! { |cookie| regexp.match? cookie }
309
-
310
- cookies.join("\n")
369
+ delete_set_cookie_header!(header, key, value)
311
370
  end
312
371
 
313
- def delete_cookie_header!(header, key, value = {})
314
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
315
- nil
372
+ def delete_cookie_header!(headers, key, value = {})
373
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
374
+
375
+ return nil
316
376
  end
317
377
 
318
- # Adds a cookie that will *remove* a cookie from the client. Hence the
319
- # strange method name.
320
378
  def add_remove_cookie_to_header(header, key, value = {})
321
- new_header = make_delete_cookie_header(header, key, value)
379
+ warn("add_remove_cookie_to_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
322
380
 
323
- add_cookie_to_header(new_header, key,
324
- { value: '', path: nil, domain: nil,
325
- max_age: '0',
326
- expires: Time.at(0) }.merge(value))
381
+ delete_set_cookie_header!(header, key, value)
382
+ end
327
383
 
384
+ # :call-seq:
385
+ # delete_set_cookie_header!(header, key, value = {}) -> header value
386
+ #
387
+ # Set an expired cookie in the specified headers with the given cookie
388
+ # +key+ and +value+ using delete_set_cookie_header. This causes
389
+ # the client to immediately delete the specified cookie.
390
+ #
391
+ # delete_set_cookie_header!(nil, "mycookie")
392
+ # # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
393
+ #
394
+ # If the header is non-nil, it will be modified in place.
395
+ #
396
+ # header = []
397
+ # delete_set_cookie_header!(header, "mycookie")
398
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
399
+ # header
400
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
401
+ #
402
+ def delete_set_cookie_header!(header, key, value = {})
403
+ if header
404
+ header = Array(header)
405
+ header << delete_set_cookie_header(key, value)
406
+ else
407
+ header = delete_set_cookie_header(key, value)
408
+ end
409
+
410
+ return header
328
411
  end
329
412
 
330
413
  def rfc2822(time)
331
414
  time.rfc2822
332
415
  end
333
416
 
334
- # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
335
- # of '% %b %Y'.
336
- # It assumes that the time is in GMT to comply to the RFC 2109.
337
- #
338
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
339
- # that I'm certain someone implemented only that option.
340
- # Do not use %a and %b from Time.strptime, it would use localized names for
341
- # weekday and month.
342
- #
343
- def rfc2109(time)
344
- wday = RFC2822_DAY_NAME[time.wday]
345
- mon = RFC2822_MONTH_NAME[time.mon - 1]
346
- time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
347
- end
348
-
349
417
  # Parses the "Range:" header, if present, into an array of Range objects.
350
418
  # Returns nil if the header is missing or syntactically invalid.
351
419
  # Returns an empty array if none of the ranges are satisfiable.
352
420
  def byte_ranges(env, size)
353
- warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
354
421
  get_byte_ranges env['HTTP_RANGE'], size
355
422
  end
356
423
 
@@ -359,18 +426,17 @@ module Rack
359
426
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
360
427
  ranges = []
361
428
  $1.split(/,\s*/).each do |range_spec|
362
- return nil unless range_spec.include?('-')
363
- range = range_spec.split('-')
364
- r0, r1 = range[0], range[1]
365
- if r0.nil? || r0.empty?
366
- return nil if r1.nil?
429
+ return nil unless range_spec =~ /(\d*)-(\d*)/
430
+ r0, r1 = $1, $2
431
+ if r0.empty?
432
+ return nil if r1.empty?
367
433
  # suffix-byte-range-spec, represents trailing suffix of file
368
434
  r0 = size - r1.to_i
369
435
  r0 = 0 if r0 < 0
370
436
  r1 = size - 1
371
437
  else
372
438
  r0 = r0.to_i
373
- if r1.nil?
439
+ if r1.empty?
374
440
  r1 = size - 1
375
441
  else
376
442
  r1 = r1.to_i
@@ -380,26 +446,33 @@ module Rack
380
446
  end
381
447
  ranges << (r0..r1) if r0 <= r1
382
448
  end
383
-
384
- return [] if ranges.map(&:size).sum > size
385
-
386
449
  ranges
387
450
  end
388
451
 
389
- # Constant time string comparison.
390
- #
391
- # NOTE: the values compared should be of fixed length, such as strings
392
- # that have already been processed by HMAC. This should not be used
393
- # on variable length plaintext strings because it could leak length info
394
- # via timing attacks.
395
- def secure_compare(a, b)
396
- return false unless a.bytesize == b.bytesize
452
+ # :nocov:
453
+ if defined?(OpenSSL.fixed_length_secure_compare)
454
+ # Constant time string comparison.
455
+ #
456
+ # NOTE: the values compared should be of fixed length, such as strings
457
+ # that have already been processed by HMAC. This should not be used
458
+ # on variable length plaintext strings because it could leak length info
459
+ # via timing attacks.
460
+ def secure_compare(a, b)
461
+ return false unless a.bytesize == b.bytesize
397
462
 
398
- l = a.unpack("C*")
463
+ OpenSSL.fixed_length_secure_compare(a, b)
464
+ end
465
+ # :nocov:
466
+ else
467
+ def secure_compare(a, b)
468
+ return false unless a.bytesize == b.bytesize
399
469
 
400
- r, i = 0, -1
401
- b.each_byte { |v| r |= v ^ l[i += 1] }
402
- r == 0
470
+ l = a.unpack("C*")
471
+
472
+ r, i = 0, -1
473
+ b.each_byte { |v| r |= v ^ l[i += 1] }
474
+ r == 0
475
+ end
403
476
  end
404
477
 
405
478
  # Context allows the use of a compatible middleware at different points
@@ -428,94 +501,32 @@ module Rack
428
501
  end
429
502
  end
430
503
 
431
- # A case-insensitive Hash that preserves the original case of a
504
+ # A wrapper around Headers
432
505
  # header when set.
433
506
  #
434
507
  # @api private
435
508
  class HeaderHash < Hash # :nodoc:
436
509
  def self.[](headers)
437
- if headers.is_a?(HeaderHash) && !headers.frozen?
510
+ warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
511
+ if headers.is_a?(Headers) && !headers.frozen?
438
512
  return headers
439
- else
440
- return self.new(headers)
441
513
  end
442
- end
443
-
444
- def initialize(hash = {})
445
- super()
446
- @names = {}
447
- hash.each { |k, v| self[k] = v }
448
- end
449
-
450
- # on dup/clone, we need to duplicate @names hash
451
- def initialize_copy(other)
452
- super
453
- @names = other.names.dup
454
- end
455
-
456
- # on clear, we need to clear @names hash
457
- def clear
458
- super
459
- @names.clear
460
- end
461
-
462
- def each
463
- super do |k, v|
464
- yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
465
- end
466
- end
467
514
 
468
- def to_hash
469
- hash = {}
470
- each { |k, v| hash[k] = v }
471
- hash
515
+ new_headers = Headers.new
516
+ headers.each{|k,v| new_headers[k] = v}
517
+ new_headers
472
518
  end
473
519
 
474
- def [](k)
475
- super(k) || super(@names[k.downcase])
520
+ def self.new(hash = {})
521
+ warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
522
+ headers = Headers.new
523
+ hash.each{|k,v| headers[k] = v}
524
+ headers
476
525
  end
477
526
 
478
- def []=(k, v)
479
- canonical = k.downcase.freeze
480
- delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
481
- @names[canonical] = k
482
- super k, v
527
+ def self.allocate
528
+ raise TypeError, "cannot allocate HeaderHash"
483
529
  end
484
-
485
- def delete(k)
486
- canonical = k.downcase
487
- result = super @names.delete(canonical)
488
- result
489
- end
490
-
491
- def include?(k)
492
- super || @names.include?(k.downcase)
493
- end
494
-
495
- alias_method :has_key?, :include?
496
- alias_method :member?, :include?
497
- alias_method :key?, :include?
498
-
499
- def merge!(other)
500
- other.each { |k, v| self[k] = v }
501
- self
502
- end
503
-
504
- def merge(other)
505
- hash = dup
506
- hash.merge! other
507
- end
508
-
509
- def replace(other)
510
- clear
511
- other.each { |k, v| self[k] = v }
512
- self
513
- end
514
-
515
- protected
516
- def names
517
- @names
518
- end
519
530
  end
520
531
 
521
532
  # Every standard HTTP code mapped to the appropriate message.