rack 2.2.17 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rack might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +501 -70
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +376 -0
- data/SPEC.rdoc +243 -277
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +5 -1
- data/lib/rack/auth/basic.rb +1 -3
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +21 -3
- data/lib/rack/builder.rb +108 -69
- data/lib/rack/cascade.rb +2 -3
- data/lib/rack/common_logger.rb +22 -17
- data/lib/rack/conditional_get.rb +20 -16
- data/lib/rack/constants.rb +68 -0
- data/lib/rack/content_length.rb +12 -16
- data/lib/rack/content_type.rb +8 -5
- data/lib/rack/deflater.rb +40 -26
- data/lib/rack/directory.rb +9 -3
- data/lib/rack/etag.rb +17 -23
- data/lib/rack/events.rb +4 -0
- data/lib/rack/files.rb +15 -17
- data/lib/rack/head.rb +8 -8
- data/lib/rack/headers.rb +238 -0
- data/lib/rack/lint.rb +817 -648
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/media_type.rb +6 -7
- data/lib/rack/method_override.rb +5 -1
- data/lib/rack/mime.rb +14 -5
- data/lib/rack/mock.rb +1 -300
- data/lib/rack/mock_request.rb +161 -0
- data/lib/rack/mock_response.rb +147 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +241 -95
- data/lib/rack/multipart/uploaded_file.rb +45 -4
- data/lib/rack/multipart.rb +53 -40
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +116 -121
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +269 -141
- data/lib/rack/response.rb +151 -66
- data/lib/rack/rewindable_input.rb +27 -5
- data/lib/rack/runtime.rb +7 -6
- data/lib/rack/sendfile.rb +30 -25
- data/lib/rack/show_exceptions.rb +25 -6
- data/lib/rack/show_status.rb +17 -9
- data/lib/rack/static.rb +8 -8
- data/lib/rack/tempfile_reaper.rb +15 -4
- data/lib/rack/urlmap.rb +3 -1
- data/lib/rack/utils.rb +228 -238
- data/lib/rack/version.rb +3 -15
- data/lib/rack.rb +13 -90
- metadata +15 -41
- data/README.rdoc +0 -347
- data/Rakefile +0 -130
- data/bin/rackup +0 -5
- data/contrib/rack.png +0 -0
- data/contrib/rack.svg +0 -150
- data/contrib/rack_logo.svg +0 -164
- data/contrib/rdoc.css +0 -412
- data/example/lobster.ru +0 -6
- data/example/protectedlobster.rb +0 -16
- data/example/protectedlobster.ru +0 -10
- data/lib/rack/auth/digest/md5.rb +0 -131
- data/lib/rack/auth/digest/nonce.rb +0 -53
- data/lib/rack/auth/digest/params.rb +0 -54
- data/lib/rack/auth/digest/request.rb +0 -43
- data/lib/rack/chunked.rb +0 -117
- data/lib/rack/core_ext/regexp.rb +0 -14
- data/lib/rack/file.rb +0 -7
- data/lib/rack/handler/cgi.rb +0 -59
- data/lib/rack/handler/fastcgi.rb +0 -100
- data/lib/rack/handler/lsws.rb +0 -61
- data/lib/rack/handler/scgi.rb +0 -71
- data/lib/rack/handler/thin.rb +0 -36
- data/lib/rack/handler/webrick.rb +0 -129
- data/lib/rack/handler.rb +0 -104
- data/lib/rack/lobster.rb +0 -70
- data/lib/rack/logger.rb +0 -20
- data/lib/rack/server.rb +0 -466
- data/lib/rack/session/abstract/id.rb +0 -523
- data/lib/rack/session/cookie.rb +0 -203
- data/lib/rack/session/memcache.rb +0 -10
- data/lib/rack/session/pool.rb +0 -90
- 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
|
33
|
-
# This helps prevent a rogue client from
|
34
|
-
|
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
|
-
|
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
|
-
|
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}[#{
|
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,23 +174,23 @@ 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
|
177
|
+
matches&.first
|
171
178
|
end
|
172
179
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
196
|
def select_best_encoding(available_encodings, accept_encoding)
|
@@ -218,24 +225,69 @@ module Rack
|
|
218
225
|
(encoding_candidates & available_encodings)[0]
|
219
226
|
end
|
220
227
|
|
221
|
-
|
222
|
-
|
223
|
-
|
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
|
224
240
|
|
225
|
-
|
226
|
-
# According to RFC 6265:
|
227
|
-
# The syntax for cookie headers only supports semicolons
|
228
|
-
# User Agent -> Server ==
|
229
|
-
# Cookie: SID=31d4d96e407aad42; lang=en-US
|
230
|
-
return {} unless header
|
231
|
-
header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
|
241
|
+
value.split(/; */n).each_with_object({}) do |cookie, cookies|
|
232
242
|
next if cookie.empty?
|
233
243
|
key, value = cookie.split('=', 2)
|
234
244
|
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
235
245
|
end
|
236
246
|
end
|
237
247
|
|
238
|
-
|
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
|
+
|
239
291
|
case value
|
240
292
|
when Hash
|
241
293
|
domain = "; domain=#{value[:domain]}" if value[:domain]
|
@@ -243,123 +295,121 @@ module Rack
|
|
243
295
|
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
244
296
|
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
245
297
|
secure = "; secure" if value[:secure]
|
246
|
-
httponly = ";
|
298
|
+
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
247
299
|
same_site =
|
248
300
|
case value[:same_site]
|
249
301
|
when false, nil
|
250
302
|
nil
|
251
303
|
when :none, 'None', :None
|
252
|
-
';
|
304
|
+
'; samesite=none'
|
253
305
|
when :lax, 'Lax', :Lax
|
254
|
-
';
|
306
|
+
'; samesite=lax'
|
255
307
|
when true, :strict, 'Strict', :Strict
|
256
|
-
';
|
308
|
+
'; samesite=strict'
|
257
309
|
else
|
258
|
-
raise ArgumentError, "Invalid
|
310
|
+
raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
|
259
311
|
end
|
312
|
+
partitioned = "; partitioned" if value[:partitioned]
|
260
313
|
value = value[:value]
|
261
314
|
end
|
315
|
+
|
262
316
|
value = [value] unless Array === value
|
263
317
|
|
264
|
-
|
265
|
-
"#{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
|
266
321
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
274
337
|
else
|
275
|
-
|
338
|
+
headers[SET_COOKIE] = set_cookie_header(key, value)
|
276
339
|
end
|
277
340
|
end
|
278
341
|
|
279
|
-
|
280
|
-
|
281
|
-
|
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: ''))
|
282
358
|
end
|
283
359
|
|
284
|
-
def
|
285
|
-
|
286
|
-
when nil, ''
|
287
|
-
cookies = []
|
288
|
-
when String
|
289
|
-
cookies = header.split("\n")
|
290
|
-
when Array
|
291
|
-
cookies = header
|
292
|
-
end
|
293
|
-
|
294
|
-
key = escape(key)
|
295
|
-
domain = value[:domain]
|
296
|
-
path = value[:path]
|
297
|
-
regexp = if domain
|
298
|
-
if path
|
299
|
-
/\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
|
300
|
-
else
|
301
|
-
/\A#{key}=.*domain=#{domain}(?:;|$)/
|
302
|
-
end
|
303
|
-
elsif path
|
304
|
-
/\A#{key}=.*path=#{path}(?:;|$)/
|
305
|
-
else
|
306
|
-
/\A#{key}=/
|
307
|
-
end
|
308
|
-
|
309
|
-
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)
|
310
362
|
|
311
|
-
|
363
|
+
return nil
|
312
364
|
end
|
313
365
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
#
|
320
|
-
#
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
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
|
328
391
|
|
392
|
+
return header
|
329
393
|
end
|
330
394
|
|
331
395
|
def rfc2822(time)
|
332
396
|
time.rfc2822
|
333
397
|
end
|
334
398
|
|
335
|
-
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
|
336
|
-
# of '% %b %Y'.
|
337
|
-
# It assumes that the time is in GMT to comply to the RFC 2109.
|
338
|
-
#
|
339
|
-
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
|
340
|
-
# that I'm certain someone implemented only that option.
|
341
|
-
# Do not use %a and %b from Time.strptime, it would use localized names for
|
342
|
-
# weekday and month.
|
343
|
-
#
|
344
|
-
def rfc2109(time)
|
345
|
-
wday = RFC2822_DAY_NAME[time.wday]
|
346
|
-
mon = RFC2822_MONTH_NAME[time.mon - 1]
|
347
|
-
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
|
348
|
-
end
|
349
|
-
|
350
399
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
351
400
|
# Returns nil if the header is missing or syntactically invalid.
|
352
401
|
# Returns an empty array if none of the ranges are satisfiable.
|
353
402
|
def byte_ranges(env, size)
|
354
|
-
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
|
355
403
|
get_byte_ranges env['HTTP_RANGE'], size
|
356
404
|
end
|
357
405
|
|
358
406
|
def get_byte_ranges(http_range, size)
|
359
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?
|
360
410
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
361
411
|
ranges = []
|
362
|
-
$1.split(
|
412
|
+
$1.split(/,[ \t]*/).each do |range_spec|
|
363
413
|
return nil unless range_spec.include?('-')
|
364
414
|
range = range_spec.split('-')
|
365
415
|
r0, r1 = range[0], range[1]
|
@@ -382,25 +432,35 @@ module Rack
|
|
382
432
|
ranges << (r0..r1) if r0 <= r1
|
383
433
|
end
|
384
434
|
|
385
|
-
return [] if ranges.map(&:size).
|
435
|
+
return [] if ranges.map(&:size).sum > size
|
386
436
|
|
387
437
|
ranges
|
388
438
|
end
|
389
439
|
|
390
|
-
#
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
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
|
398
450
|
|
399
|
-
|
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
|
400
457
|
|
401
|
-
|
402
|
-
|
403
|
-
|
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
|
404
464
|
end
|
405
465
|
|
406
466
|
# Context allows the use of a compatible middleware at different points
|
@@ -429,101 +489,12 @@ module Rack
|
|
429
489
|
end
|
430
490
|
end
|
431
491
|
|
432
|
-
# A case-insensitive Hash that preserves the original case of a
|
433
|
-
# header when set.
|
434
|
-
#
|
435
|
-
# @api private
|
436
|
-
class HeaderHash < Hash # :nodoc:
|
437
|
-
def self.[](headers)
|
438
|
-
if headers.is_a?(HeaderHash) && !headers.frozen?
|
439
|
-
return headers
|
440
|
-
else
|
441
|
-
return self.new(headers)
|
442
|
-
end
|
443
|
-
end
|
444
|
-
|
445
|
-
def initialize(hash = {})
|
446
|
-
super()
|
447
|
-
@names = {}
|
448
|
-
hash.each { |k, v| self[k] = v }
|
449
|
-
end
|
450
|
-
|
451
|
-
# on dup/clone, we need to duplicate @names hash
|
452
|
-
def initialize_copy(other)
|
453
|
-
super
|
454
|
-
@names = other.names.dup
|
455
|
-
end
|
456
|
-
|
457
|
-
# on clear, we need to clear @names hash
|
458
|
-
def clear
|
459
|
-
super
|
460
|
-
@names.clear
|
461
|
-
end
|
462
|
-
|
463
|
-
def each
|
464
|
-
super do |k, v|
|
465
|
-
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
466
|
-
end
|
467
|
-
end
|
468
|
-
|
469
|
-
def to_hash
|
470
|
-
hash = {}
|
471
|
-
each { |k, v| hash[k] = v }
|
472
|
-
hash
|
473
|
-
end
|
474
|
-
|
475
|
-
def [](k)
|
476
|
-
super(k) || super(@names[k.downcase])
|
477
|
-
end
|
478
|
-
|
479
|
-
def []=(k, v)
|
480
|
-
canonical = k.downcase.freeze
|
481
|
-
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
|
482
|
-
@names[canonical] = k
|
483
|
-
super k, v
|
484
|
-
end
|
485
|
-
|
486
|
-
def delete(k)
|
487
|
-
canonical = k.downcase
|
488
|
-
result = super @names.delete(canonical)
|
489
|
-
result
|
490
|
-
end
|
491
|
-
|
492
|
-
def include?(k)
|
493
|
-
super || @names.include?(k.downcase)
|
494
|
-
end
|
495
|
-
|
496
|
-
alias_method :has_key?, :include?
|
497
|
-
alias_method :member?, :include?
|
498
|
-
alias_method :key?, :include?
|
499
|
-
|
500
|
-
def merge!(other)
|
501
|
-
other.each { |k, v| self[k] = v }
|
502
|
-
self
|
503
|
-
end
|
504
|
-
|
505
|
-
def merge(other)
|
506
|
-
hash = dup
|
507
|
-
hash.merge! other
|
508
|
-
end
|
509
|
-
|
510
|
-
def replace(other)
|
511
|
-
clear
|
512
|
-
other.each { |k, v| self[k] = v }
|
513
|
-
self
|
514
|
-
end
|
515
|
-
|
516
|
-
protected
|
517
|
-
def names
|
518
|
-
@names
|
519
|
-
end
|
520
|
-
end
|
521
|
-
|
522
492
|
# Every standard HTTP code mapped to the appropriate message.
|
523
493
|
# Generated with:
|
524
|
-
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv
|
525
|
-
# ruby -
|
526
|
-
#
|
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)"
|
527
498
|
HTTP_STATUS_CODES = {
|
528
499
|
100 => 'Continue',
|
529
500
|
101 => 'Switching Protocols',
|
@@ -545,7 +516,6 @@ module Rack
|
|
545
516
|
303 => 'See Other',
|
546
517
|
304 => 'Not Modified',
|
547
518
|
305 => 'Use Proxy',
|
548
|
-
306 => '(Unused)',
|
549
519
|
307 => 'Temporary Redirect',
|
550
520
|
308 => 'Permanent Redirect',
|
551
521
|
400 => 'Bad Request',
|
@@ -561,13 +531,13 @@ module Rack
|
|
561
531
|
410 => 'Gone',
|
562
532
|
411 => 'Length Required',
|
563
533
|
412 => 'Precondition Failed',
|
564
|
-
413 => '
|
534
|
+
413 => 'Content Too Large',
|
565
535
|
414 => 'URI Too Long',
|
566
536
|
415 => 'Unsupported Media Type',
|
567
537
|
416 => 'Range Not Satisfiable',
|
568
538
|
417 => 'Expectation Failed',
|
569
539
|
421 => 'Misdirected Request',
|
570
|
-
422 => 'Unprocessable
|
540
|
+
422 => 'Unprocessable Content',
|
571
541
|
423 => 'Locked',
|
572
542
|
424 => 'Failed Dependency',
|
573
543
|
425 => 'Too Early',
|
@@ -575,7 +545,7 @@ module Rack
|
|
575
545
|
428 => 'Precondition Required',
|
576
546
|
429 => 'Too Many Requests',
|
577
547
|
431 => 'Request Header Fields Too Large',
|
578
|
-
451 => 'Unavailable
|
548
|
+
451 => 'Unavailable For Legal Reasons',
|
579
549
|
500 => 'Internal Server Error',
|
580
550
|
501 => 'Not Implemented',
|
581
551
|
502 => 'Bad Gateway',
|
@@ -585,8 +555,6 @@ module Rack
|
|
585
555
|
506 => 'Variant Also Negotiates',
|
586
556
|
507 => 'Insufficient Storage',
|
587
557
|
508 => 'Loop Detected',
|
588
|
-
509 => 'Bandwidth Limit Exceeded',
|
589
|
-
510 => 'Not Extended',
|
590
558
|
511 => 'Network Authentication Required'
|
591
559
|
}
|
592
560
|
|
@@ -594,12 +562,34 @@ module Rack
|
|
594
562
|
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
595
563
|
|
596
564
|
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
597
|
-
[message.downcase.gsub(/\s
|
565
|
+
[message.downcase.gsub(/\s|-/, '_').to_sym, code]
|
598
566
|
}.flatten]
|
599
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
|
+
|
600
582
|
def status_code(status)
|
601
583
|
if status.is_a?(Symbol)
|
602
|
-
SYMBOL_TO_STATUS_CODE.fetch(status)
|
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
|
603
593
|
else
|
604
594
|
status.to_i
|
605
595
|
end
|