rack 2.1.0 → 3.1.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.

Potentially problematic release.


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

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