rack 2.2.23 → 3.2.5

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +554 -83
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +384 -0
  6. data/SPEC.rdoc +243 -277
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +5 -1
  9. data/lib/rack/auth/basic.rb +1 -3
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +21 -3
  12. data/lib/rack/builder.rb +108 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +22 -17
  15. data/lib/rack/conditional_get.rb +20 -16
  16. data/lib/rack/constants.rb +68 -0
  17. data/lib/rack/content_length.rb +12 -16
  18. data/lib/rack/content_type.rb +8 -5
  19. data/lib/rack/deflater.rb +40 -26
  20. data/lib/rack/directory.rb +10 -4
  21. data/lib/rack/etag.rb +17 -23
  22. data/lib/rack/events.rb +25 -6
  23. data/lib/rack/files.rb +16 -18
  24. data/lib/rack/head.rb +8 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +817 -648
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/media_type.rb +6 -7
  29. data/lib/rack/method_override.rb +5 -1
  30. data/lib/rack/mime.rb +14 -5
  31. data/lib/rack/mock.rb +1 -300
  32. data/lib/rack/mock_request.rb +161 -0
  33. data/lib/rack/mock_response.rb +156 -0
  34. data/lib/rack/multipart/generator.rb +7 -5
  35. data/lib/rack/multipart/parser.rb +240 -123
  36. data/lib/rack/multipart/uploaded_file.rb +45 -4
  37. data/lib/rack/multipart.rb +53 -40
  38. data/lib/rack/null_logger.rb +9 -0
  39. data/lib/rack/query_parser.rb +116 -121
  40. data/lib/rack/recursive.rb +2 -0
  41. data/lib/rack/reloader.rb +0 -2
  42. data/lib/rack/request.rb +272 -144
  43. data/lib/rack/response.rb +151 -66
  44. data/lib/rack/rewindable_input.rb +27 -5
  45. data/lib/rack/runtime.rb +7 -6
  46. data/lib/rack/sendfile.rb +37 -32
  47. data/lib/rack/show_exceptions.rb +25 -6
  48. data/lib/rack/show_status.rb +17 -9
  49. data/lib/rack/static.rb +11 -15
  50. data/lib/rack/tempfile_reaper.rb +15 -4
  51. data/lib/rack/urlmap.rb +3 -1
  52. data/lib/rack/utils.rb +234 -275
  53. data/lib/rack/version.rb +3 -15
  54. data/lib/rack.rb +13 -90
  55. metadata +15 -41
  56. data/README.rdoc +0 -355
  57. data/Rakefile +0 -130
  58. data/bin/rackup +0 -5
  59. data/contrib/rack.png +0 -0
  60. data/contrib/rack.svg +0 -150
  61. data/contrib/rack_logo.svg +0 -164
  62. data/contrib/rdoc.css +0 -412
  63. data/example/lobster.ru +0 -6
  64. data/example/protectedlobster.rb +0 -16
  65. data/example/protectedlobster.ru +0 -10
  66. data/lib/rack/auth/digest/md5.rb +0 -131
  67. data/lib/rack/auth/digest/nonce.rb +0 -53
  68. data/lib/rack/auth/digest/params.rb +0 -54
  69. data/lib/rack/auth/digest/request.rb +0 -43
  70. data/lib/rack/chunked.rb +0 -117
  71. data/lib/rack/core_ext/regexp.rb +0 -14
  72. data/lib/rack/file.rb +0 -7
  73. data/lib/rack/handler/cgi.rb +0 -59
  74. data/lib/rack/handler/fastcgi.rb +0 -100
  75. data/lib/rack/handler/lsws.rb +0 -61
  76. data/lib/rack/handler/scgi.rb +0 -71
  77. data/lib/rack/handler/thin.rb +0 -34
  78. data/lib/rack/handler/webrick.rb +0 -129
  79. data/lib/rack/handler.rb +0 -104
  80. data/lib/rack/lobster.rb +0 -70
  81. data/lib/rack/logger.rb +0 -20
  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 -90
  87. data/rack.gemspec +0 -46
data/lib/rack/utils.rb CHANGED
@@ -6,32 +6,33 @@ require 'fileutils'
6
6
  require 'set'
7
7
  require 'tempfile'
8
8
  require 'time'
9
+ require 'erb'
9
10
 
10
11
  require_relative 'query_parser'
12
+ require_relative 'mime'
13
+ require_relative 'headers'
14
+ require_relative 'constants'
11
15
 
12
16
  module Rack
13
17
  # Rack::Utils contains a grab-bag of useful methods for writing web
14
18
  # applications adopted from all kinds of Ruby libraries.
15
19
 
16
20
  module Utils
17
- (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
18
-
19
21
  ParameterTypeError = QueryParser::ParameterTypeError
20
22
  InvalidParameterError = QueryParser::InvalidParameterError
23
+ ParamsTooDeepError = QueryParser::ParamsTooDeepError
21
24
  DEFAULT_SEP = QueryParser::DEFAULT_SEP
22
25
  COMMON_SEP = QueryParser::COMMON_SEP
23
26
  KeySpaceConstrainedParams = QueryParser::Params
24
-
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
- RFC2396_PARSER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::RFC2396_Parser.new
27
+ URI_PARSER = defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER
28
28
 
29
29
  class << self
30
30
  attr_accessor :default_query_parser
31
31
  end
32
- # The default number of bytes to allow parameter keys to take up.
33
- # This helps prevent a rogue client from flooding a Request.
34
- self.default_query_parser = QueryParser.make_default(65536, 100)
32
+ # The default amount of nesting to allowed by hash parameters.
33
+ # This helps prevent a rogue client from triggering a possible stack overflow
34
+ # when parsing parameters.
35
+ self.default_query_parser = QueryParser.make_default(32)
35
36
 
36
37
  module_function
37
38
 
@@ -43,13 +44,13 @@ module Rack
43
44
  # Like URI escaping, but with %20 instead of +. Strictly speaking this is
44
45
  # true URI escaping.
45
46
  def escape_path(s)
46
- RFC2396_PARSER.escape s
47
+ URI_PARSER.escape s
47
48
  end
48
49
 
49
50
  # Unescapes the **path** component of a URI. See Rack::Utils.unescape for
50
51
  # unescaping query parameters or form components.
51
52
  def unescape_path(s)
52
- RFC2396_PARSER.unescape s
53
+ URI_PARSER.unescape s
53
54
  end
54
55
 
55
56
  # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
@@ -86,14 +87,6 @@ module Rack
86
87
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
87
88
  end
88
89
 
89
- def self.key_space_limit
90
- default_query_parser.key_space_limit
91
- end
92
-
93
- def self.key_space_limit=(v)
94
- self.default_query_parser = self.default_query_parser.new_space_limit(v)
95
- end
96
-
97
90
  if defined?(Process::CLOCK_MONOTONIC)
98
91
  def clock_time
99
92
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -132,13 +125,13 @@ module Rack
132
125
  }.join("&")
133
126
  when Hash
134
127
  value.map { |k, v|
135
- build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
128
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
136
129
  }.delete_if(&:empty?).join('&')
137
130
  when nil
138
- prefix
131
+ escape(prefix)
139
132
  else
140
133
  raise ArgumentError, "value must be a Hash" if prefix.nil?
141
- "#{prefix}=#{escape(value)}"
134
+ "#{escape(prefix)}=#{escape(value)}"
142
135
  end
143
136
  end
144
137
 
@@ -153,6 +146,20 @@ module Rack
153
146
  end
154
147
  end
155
148
 
149
+ def forwarded_values(forwarded_header)
150
+ return nil unless forwarded_header
151
+ forwarded_header = forwarded_header.to_s.gsub("\n", ";")
152
+
153
+ forwarded_header.split(';').each_with_object({}) do |field, values|
154
+ field.split(',').each do |pair|
155
+ pair = pair.split('=').map(&:strip).join('=')
156
+ return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
157
+ (values[$1.downcase.to_sym] ||= []) << $2
158
+ end
159
+ end
160
+ end
161
+ module_function :forwarded_values
162
+
156
163
  # Return best accept value to use, based on the algorithm
157
164
  # in RFC 2616 Section 14. If there are multiple best
158
165
  # matches (same specificity and quality), the value returned
@@ -167,60 +174,36 @@ module Rack
167
174
  end.compact.sort_by do |match, quality|
168
175
  (match.split('/', 2).count('*') * -10) + quality
169
176
  end.last
170
- matches && matches.first
177
+ matches&.first
171
178
  end
172
179
 
173
- ESCAPE_HTML = {
174
- "&" => "&amp;",
175
- "<" => "&lt;",
176
- ">" => "&gt;",
177
- "'" => "&#x27;",
178
- '"' => "&quot;",
179
- "/" => "&#x2F;"
180
- }
181
-
182
- ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
183
-
184
- # Escape ampersands, brackets and quotes to their HTML/XML entities.
185
- def escape_html(string)
186
- string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
180
+ # Introduced in ERB 4.0. ERB::Escape is an alias for ERB::Utils which
181
+ # doesn't get monkey-patched by rails
182
+ if defined?(ERB::Escape) && ERB::Escape.instance_method(:html_escape)
183
+ define_method(:escape_html, ERB::Escape.instance_method(:html_escape))
184
+ # :nocov:
185
+ # Ruby 3.2/ERB 4.0 added ERB::Escape#html_escape, so the else
186
+ # branch cannot be hit on the current Ruby version.
187
+ else
188
+ require 'cgi/escape'
189
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
190
+ def escape_html(string)
191
+ CGI.escapeHTML(string.to_s)
192
+ end
193
+ # :nocov:
187
194
  end
188
195
 
189
- # Given an array of available encoding strings, and an array of
190
- # acceptable encodings for a request, where each element of the
191
- # acceptable encodings array is an array where the first element
192
- # is an encoding name and the second element is the numeric
193
- # priority for the encoding, return the available encoding with
194
- # the highest priority.
195
- #
196
- # The accept_encoding argument is typically generated by calling
197
- # Request#accept_encoding.
198
- #
199
- # Example:
200
- #
201
- # select_best_encoding(%w(compress gzip identity),
202
- # [["compress", 0.5], ["gzip", 1.0]])
203
- # # => "gzip"
204
- #
205
- # To reduce denial of service potential, only the first 16
206
- # acceptable encodings are considered.
207
196
  def select_best_encoding(available_encodings, accept_encoding)
208
197
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
209
198
 
210
- # Only process the first 16 encodings
211
- accept_encoding = accept_encoding[0...16]
212
199
  expanded_accept_encoding = []
213
- wildcard_seen = false
214
200
 
215
201
  accept_encoding.each do |m, q|
216
202
  preference = available_encodings.index(m) || available_encodings.size
217
203
 
218
204
  if m == "*"
219
- unless wildcard_seen
220
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
221
- expanded_accept_encoding << [m2, q, preference]
222
- end
223
- wildcard_seen = true
205
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
206
+ expanded_accept_encoding << [m2, q, preference]
224
207
  end
225
208
  else
226
209
  expanded_accept_encoding << [m, q, preference]
@@ -228,13 +211,7 @@ module Rack
228
211
  end
229
212
 
230
213
  encoding_candidates = expanded_accept_encoding
231
- .sort do |(_, q1, p1), (_, q2, p2)|
232
- if r = (q1 <=> q2).nonzero?
233
- -r
234
- else
235
- (p1 <=> p2).nonzero? || 0
236
- end
237
- end
214
+ .sort_by { |_, q, p| [-q, p] }
238
215
  .map!(&:first)
239
216
 
240
217
  unless encoding_candidates.include?("identity")
@@ -248,24 +225,69 @@ module Rack
248
225
  (encoding_candidates & available_encodings)[0]
249
226
  end
250
227
 
251
- def parse_cookies(env)
252
- parse_cookies_header env[HTTP_COOKIE]
253
- end
228
+ # :call-seq:
229
+ # parse_cookies_header(value) -> hash
230
+ #
231
+ # Parse cookies from the provided header +value+ according to RFC6265. The
232
+ # syntax for cookie headers only supports semicolons. Returns a map of
233
+ # cookie +key+ to cookie +value+.
234
+ #
235
+ # parse_cookies_header('myname=myvalue; max-age=0')
236
+ # # => {"myname"=>"myvalue", "max-age"=>"0"}
237
+ #
238
+ def parse_cookies_header(value)
239
+ return {} unless value
254
240
 
255
- def parse_cookies_header(header)
256
- # According to RFC 6265:
257
- # The syntax for cookie headers only supports semicolons
258
- # User Agent -> Server ==
259
- # Cookie: SID=31d4d96e407aad42; lang=en-US
260
- return {} unless header
261
- header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
241
+ value.split(/; */n).each_with_object({}) do |cookie, cookies|
262
242
  next if cookie.empty?
263
243
  key, value = cookie.split('=', 2)
264
244
  cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
265
245
  end
266
246
  end
267
247
 
268
- def add_cookie_to_header(header, key, value)
248
+ # :call-seq:
249
+ # parse_cookies(env) -> hash
250
+ #
251
+ # Parse cookies from the provided request environment using
252
+ # parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
253
+ #
254
+ # parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
255
+ # # => {'myname' => 'myvalue'}
256
+ #
257
+ def parse_cookies(env)
258
+ parse_cookies_header env[HTTP_COOKIE]
259
+ end
260
+
261
+ # A valid cookie key according to RFC6265 and RFC2616.
262
+ # A <cookie-name> can be any US-ASCII characters, except control characters, spaces, or tabs. It also must not contain a separator character like the following: ( ) < > @ , ; : \ " / [ ] ? = { }.
263
+ VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
264
+ private_constant :VALID_COOKIE_KEY
265
+
266
+ # :call-seq:
267
+ # set_cookie_header(key, value) -> encoded string
268
+ #
269
+ # Generate an encoded string using the provided +key+ and +value+ suitable
270
+ # for the +set-cookie+ header according to RFC6265. The +value+ may be an
271
+ # instance of either +String+ or +Hash+. If the cookie key is invalid (as
272
+ # defined by RFC6265), an +ArgumentError+ will be raised.
273
+ #
274
+ # If the cookie +value+ is an instance of +Hash+, it considers the following
275
+ # cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
276
+ # of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
277
+ # details about the interpretation of these fields, consult
278
+ # [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
279
+ #
280
+ # set_cookie_header("myname", "myvalue")
281
+ # # => "myname=myvalue"
282
+ #
283
+ # set_cookie_header("myname", {value: "myvalue", max_age: 10})
284
+ # # => "myname=myvalue; max-age=10"
285
+ #
286
+ def set_cookie_header(key, value)
287
+ unless key =~ VALID_COOKIE_KEY
288
+ raise ArgumentError, "invalid cookie key: #{key.inspect}"
289
+ end
290
+
269
291
  case value
270
292
  when Hash
271
293
  domain = "; domain=#{value[:domain]}" if value[:domain]
@@ -273,124 +295,121 @@ module Rack
273
295
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
274
296
  expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
275
297
  secure = "; secure" if value[:secure]
276
- httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
298
+ httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
277
299
  same_site =
278
300
  case value[:same_site]
279
301
  when false, nil
280
302
  nil
281
303
  when :none, 'None', :None
282
- '; SameSite=None'
304
+ '; samesite=none'
283
305
  when :lax, 'Lax', :Lax
284
- '; SameSite=Lax'
306
+ '; samesite=lax'
285
307
  when true, :strict, 'Strict', :Strict
286
- '; SameSite=Strict'
308
+ '; samesite=strict'
287
309
  else
288
- raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
310
+ raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
289
311
  end
312
+ partitioned = "; partitioned" if value[:partitioned]
290
313
  value = value[:value]
291
314
  end
315
+
292
316
  value = [value] unless Array === value
293
317
 
294
- cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
295
- "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
318
+ return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
319
+ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
320
+ end
296
321
 
297
- case header
298
- when nil, ''
299
- cookie
300
- when String
301
- [header, cookie].join("\n")
302
- when Array
303
- (header + [cookie]).join("\n")
322
+ # :call-seq:
323
+ # set_cookie_header!(headers, key, value) -> header value
324
+ #
325
+ # Append a cookie in the specified headers with the given cookie +key+ and
326
+ # +value+ using set_cookie_header.
327
+ #
328
+ # If the headers already contains a +set-cookie+ key, it will be converted
329
+ # to an +Array+ if not already, and appended to.
330
+ def set_cookie_header!(headers, key, value)
331
+ if header = headers[SET_COOKIE]
332
+ if header.is_a?(Array)
333
+ header << set_cookie_header(key, value)
334
+ else
335
+ headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
336
+ end
304
337
  else
305
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
338
+ headers[SET_COOKIE] = set_cookie_header(key, value)
306
339
  end
307
340
  end
308
341
 
309
- def set_cookie_header!(header, key, value)
310
- header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
311
- nil
342
+ # :call-seq:
343
+ # delete_set_cookie_header(key, value = {}) -> encoded string
344
+ #
345
+ # Generate an encoded string based on the given +key+ and +value+ using
346
+ # set_cookie_header for the purpose of causing the specified cookie to be
347
+ # deleted. The +value+ may be an instance of +Hash+ and can include
348
+ # attributes as outlined by set_cookie_header. The encoded cookie will have
349
+ # a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
350
+ # +value+. When used with the +set-cookie+ header, it will cause the client
351
+ # to *remove* any matching cookie.
352
+ #
353
+ # delete_set_cookie_header("myname")
354
+ # # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
355
+ #
356
+ def delete_set_cookie_header(key, value = {})
357
+ set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
312
358
  end
313
359
 
314
- def make_delete_cookie_header(header, key, value)
315
- case header
316
- when nil, ''
317
- cookies = []
318
- when String
319
- cookies = header.split("\n")
320
- when Array
321
- cookies = header
322
- end
323
-
324
- key = escape(key)
325
- domain = value[:domain]
326
- path = value[:path]
327
- regexp = if domain
328
- if path
329
- /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
330
- else
331
- /\A#{key}=.*domain=#{domain}(?:;|$)/
332
- end
333
- elsif path
334
- /\A#{key}=.*path=#{path}(?:;|$)/
335
- else
336
- /\A#{key}=/
337
- end
338
-
339
- cookies.reject! { |cookie| regexp.match? cookie }
360
+ def delete_cookie_header!(headers, key, value = {})
361
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
340
362
 
341
- cookies.join("\n")
363
+ return nil
342
364
  end
343
365
 
344
- def delete_cookie_header!(header, key, value = {})
345
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
346
- nil
347
- end
348
-
349
- # Adds a cookie that will *remove* a cookie from the client. Hence the
350
- # strange method name.
351
- def add_remove_cookie_to_header(header, key, value = {})
352
- new_header = make_delete_cookie_header(header, key, value)
353
-
354
- add_cookie_to_header(new_header, key,
355
- { value: '', path: nil, domain: nil,
356
- max_age: '0',
357
- expires: Time.at(0) }.merge(value))
366
+ # :call-seq:
367
+ # delete_set_cookie_header!(header, key, value = {}) -> header value
368
+ #
369
+ # Set an expired cookie in the specified headers with the given cookie
370
+ # +key+ and +value+ using delete_set_cookie_header. This causes
371
+ # the client to immediately delete the specified cookie.
372
+ #
373
+ # delete_set_cookie_header!(nil, "mycookie")
374
+ # # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
375
+ #
376
+ # If the header is non-nil, it will be modified in place.
377
+ #
378
+ # header = []
379
+ # delete_set_cookie_header!(header, "mycookie")
380
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
381
+ # header
382
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
383
+ #
384
+ def delete_set_cookie_header!(header, key, value = {})
385
+ if header
386
+ header = Array(header)
387
+ header << delete_set_cookie_header(key, value)
388
+ else
389
+ header = delete_set_cookie_header(key, value)
390
+ end
358
391
 
392
+ return header
359
393
  end
360
394
 
361
395
  def rfc2822(time)
362
396
  time.rfc2822
363
397
  end
364
398
 
365
- # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
366
- # of '% %b %Y'.
367
- # It assumes that the time is in GMT to comply to the RFC 2109.
368
- #
369
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
370
- # that I'm certain someone implemented only that option.
371
- # Do not use %a and %b from Time.strptime, it would use localized names for
372
- # weekday and month.
373
- #
374
- def rfc2109(time)
375
- wday = RFC2822_DAY_NAME[time.wday]
376
- mon = RFC2822_MONTH_NAME[time.mon - 1]
377
- time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
378
- end
379
-
380
399
  # Parses the "Range:" header, if present, into an array of Range objects.
381
400
  # Returns nil if the header is missing or syntactically invalid.
382
401
  # Returns an empty array if none of the ranges are satisfiable.
383
- def byte_ranges(env, size, max_ranges: 100)
384
- get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
402
+ def byte_ranges(env, size)
403
+ get_byte_ranges env['HTTP_RANGE'], size
385
404
  end
386
405
 
387
- def get_byte_ranges(http_range, size, max_ranges: 100)
406
+ def get_byte_ranges(http_range, size)
388
407
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
408
+ # Ignore Range when file size is 0 to avoid a 416 error.
409
+ return nil if size.zero?
389
410
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
390
- byte_range = $1
391
- return nil if byte_range.count(',') >= max_ranges
392
411
  ranges = []
393
- byte_range.split(/,[ \t]*/).each do |range_spec|
412
+ $1.split(/,[ \t]*/).each do |range_spec|
394
413
  return nil unless range_spec.include?('-')
395
414
  range = range_spec.split('-')
396
415
  r0, r1 = range[0], range[1]
@@ -413,25 +432,35 @@ module Rack
413
432
  ranges << (r0..r1) if r0 <= r1
414
433
  end
415
434
 
416
- return [] if ranges.map(&:size).inject(0, :+) > size
435
+ return [] if ranges.map(&:size).sum > size
417
436
 
418
437
  ranges
419
438
  end
420
439
 
421
- # Constant time string comparison.
422
- #
423
- # NOTE: the values compared should be of fixed length, such as strings
424
- # that have already been processed by HMAC. This should not be used
425
- # on variable length plaintext strings because it could leak length info
426
- # via timing attacks.
427
- def secure_compare(a, b)
428
- return false unless a.bytesize == b.bytesize
440
+ # :nocov:
441
+ if defined?(OpenSSL.fixed_length_secure_compare)
442
+ # Constant time string comparison.
443
+ #
444
+ # NOTE: the values compared should be of fixed length, such as strings
445
+ # that have already been processed by HMAC. This should not be used
446
+ # on variable length plaintext strings because it could leak length info
447
+ # via timing attacks.
448
+ def secure_compare(a, b)
449
+ return false unless a.bytesize == b.bytesize
429
450
 
430
- l = a.unpack("C*")
451
+ OpenSSL.fixed_length_secure_compare(a, b)
452
+ end
453
+ # :nocov:
454
+ else
455
+ def secure_compare(a, b)
456
+ return false unless a.bytesize == b.bytesize
431
457
 
432
- r, i = 0, -1
433
- b.each_byte { |v| r |= v ^ l[i += 1] }
434
- r == 0
458
+ l = a.unpack("C*")
459
+
460
+ r, i = 0, -1
461
+ b.each_byte { |v| r |= v ^ l[i += 1] }
462
+ r == 0
463
+ end
435
464
  end
436
465
 
437
466
  # Context allows the use of a compatible middleware at different points
@@ -460,101 +489,12 @@ module Rack
460
489
  end
461
490
  end
462
491
 
463
- # A case-insensitive Hash that preserves the original case of a
464
- # header when set.
465
- #
466
- # @api private
467
- class HeaderHash < Hash # :nodoc:
468
- def self.[](headers)
469
- if headers.is_a?(HeaderHash) && !headers.frozen?
470
- return headers
471
- else
472
- return self.new(headers)
473
- end
474
- end
475
-
476
- def initialize(hash = {})
477
- super()
478
- @names = {}
479
- hash.each { |k, v| self[k] = v }
480
- end
481
-
482
- # on dup/clone, we need to duplicate @names hash
483
- def initialize_copy(other)
484
- super
485
- @names = other.names.dup
486
- end
487
-
488
- # on clear, we need to clear @names hash
489
- def clear
490
- super
491
- @names.clear
492
- end
493
-
494
- def each
495
- super do |k, v|
496
- yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
497
- end
498
- end
499
-
500
- def to_hash
501
- hash = {}
502
- each { |k, v| hash[k] = v }
503
- hash
504
- end
505
-
506
- def [](k)
507
- super(k) || super(@names[k.downcase])
508
- end
509
-
510
- def []=(k, v)
511
- canonical = k.downcase.freeze
512
- delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
513
- @names[canonical] = k
514
- super k, v
515
- end
516
-
517
- def delete(k)
518
- canonical = k.downcase
519
- result = super @names.delete(canonical)
520
- result
521
- end
522
-
523
- def include?(k)
524
- super || @names.include?(k.downcase)
525
- end
526
-
527
- alias_method :has_key?, :include?
528
- alias_method :member?, :include?
529
- alias_method :key?, :include?
530
-
531
- def merge!(other)
532
- other.each { |k, v| self[k] = v }
533
- self
534
- end
535
-
536
- def merge(other)
537
- hash = dup
538
- hash.merge! other
539
- end
540
-
541
- def replace(other)
542
- clear
543
- other.each { |k, v| self[k] = v }
544
- self
545
- end
546
-
547
- protected
548
- def names
549
- @names
550
- end
551
- end
552
-
553
492
  # Every standard HTTP code mapped to the appropriate message.
554
493
  # Generated with:
555
- # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
556
- # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
557
- # puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
494
+ # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
495
+ # | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
496
+ # .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
497
+ # .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
558
498
  HTTP_STATUS_CODES = {
559
499
  100 => 'Continue',
560
500
  101 => 'Switching Protocols',
@@ -576,7 +516,6 @@ module Rack
576
516
  303 => 'See Other',
577
517
  304 => 'Not Modified',
578
518
  305 => 'Use Proxy',
579
- 306 => '(Unused)',
580
519
  307 => 'Temporary Redirect',
581
520
  308 => 'Permanent Redirect',
582
521
  400 => 'Bad Request',
@@ -592,13 +531,13 @@ module Rack
592
531
  410 => 'Gone',
593
532
  411 => 'Length Required',
594
533
  412 => 'Precondition Failed',
595
- 413 => 'Payload Too Large',
534
+ 413 => 'Content Too Large',
596
535
  414 => 'URI Too Long',
597
536
  415 => 'Unsupported Media Type',
598
537
  416 => 'Range Not Satisfiable',
599
538
  417 => 'Expectation Failed',
600
539
  421 => 'Misdirected Request',
601
- 422 => 'Unprocessable Entity',
540
+ 422 => 'Unprocessable Content',
602
541
  423 => 'Locked',
603
542
  424 => 'Failed Dependency',
604
543
  425 => 'Too Early',
@@ -606,7 +545,7 @@ module Rack
606
545
  428 => 'Precondition Required',
607
546
  429 => 'Too Many Requests',
608
547
  431 => 'Request Header Fields Too Large',
609
- 451 => 'Unavailable for Legal Reasons',
548
+ 451 => 'Unavailable For Legal Reasons',
610
549
  500 => 'Internal Server Error',
611
550
  501 => 'Not Implemented',
612
551
  502 => 'Bad Gateway',
@@ -616,8 +555,6 @@ module Rack
616
555
  506 => 'Variant Also Negotiates',
617
556
  507 => 'Insufficient Storage',
618
557
  508 => 'Loop Detected',
619
- 509 => 'Bandwidth Limit Exceeded',
620
- 510 => 'Not Extended',
621
558
  511 => 'Network Authentication Required'
622
559
  }
623
560
 
@@ -625,12 +562,34 @@ module Rack
625
562
  STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
626
563
 
627
564
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
628
- [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
565
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
629
566
  }.flatten]
630
567
 
568
+ OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
569
+ payload_too_large: 413,
570
+ unprocessable_entity: 422,
571
+ bandwidth_limit_exceeded: 509,
572
+ not_extended: 510
573
+ }.freeze
574
+ private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
575
+
576
+ OBSOLETE_SYMBOL_MAPPINGS = {
577
+ payload_too_large: :content_too_large,
578
+ unprocessable_entity: :unprocessable_content
579
+ }.freeze
580
+ private_constant :OBSOLETE_SYMBOL_MAPPINGS
581
+
631
582
  def status_code(status)
632
583
  if status.is_a?(Symbol)
633
- SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
584
+ SYMBOL_TO_STATUS_CODE.fetch(status) do
585
+ fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
586
+ message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
587
+ if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
588
+ message = "#{message} Please use #{canonical_symbol.inspect} instead."
589
+ end
590
+ warn message, uplevel: 3
591
+ fallback_code
592
+ end
634
593
  else
635
594
  status.to_i
636
595
  end