rack 2.2.7 → 3.1.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +291 -78
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +328 -0
- data/SPEC.rdoc +213 -136
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +3 -1
- data/lib/rack/auth/basic.rb +1 -4
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +21 -3
- data/lib/rack/builder.rb +102 -69
- data/lib/rack/cascade.rb +2 -3
- data/lib/rack/common_logger.rb +23 -18
- data/lib/rack/conditional_get.rb +18 -15
- data/lib/rack/constants.rb +67 -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 +14 -23
- data/lib/rack/events.rb +4 -0
- data/lib/rack/files.rb +15 -17
- data/lib/rack/head.rb +9 -8
- data/lib/rack/headers.rb +238 -0
- data/lib/rack/lint.rb +864 -681
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/logger.rb +3 -0
- data/lib/rack/media_type.rb +9 -4
- data/lib/rack/method_override.rb +5 -1
- data/lib/rack/mime.rb +14 -5
- data/lib/rack/mock.rb +1 -271
- data/lib/rack/mock_request.rb +171 -0
- data/lib/rack/mock_response.rb +124 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +218 -91
- data/lib/rack/multipart/uploaded_file.rb +4 -0
- data/lib/rack/multipart.rb +53 -40
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +81 -102
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +248 -123
- data/lib/rack/response.rb +146 -66
- data/lib/rack/rewindable_input.rb +24 -5
- data/lib/rack/runtime.rb +7 -6
- data/lib/rack/sendfile.rb +30 -25
- data/lib/rack/show_exceptions.rb +21 -4
- data/lib/rack/show_status.rb +17 -7
- 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 +237 -235
- data/lib/rack/version.rb +1 -9
- data/lib/rack.rb +13 -89
- metadata +15 -41
- data/README.rdoc +0 -320
- 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 -54
- 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/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 -85
- data/rack.gemspec +0 -46
data/lib/rack/utils.rb
CHANGED
@@ -6,31 +6,32 @@ 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
27
|
|
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
28
|
class << self
|
29
29
|
attr_accessor :default_query_parser
|
30
30
|
end
|
31
|
-
# The default
|
32
|
-
# This helps prevent a rogue client from
|
33
|
-
|
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)
|
34
35
|
|
35
36
|
module_function
|
36
37
|
|
@@ -85,14 +86,6 @@ module Rack
|
|
85
86
|
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
|
86
87
|
end
|
87
88
|
|
88
|
-
def self.key_space_limit
|
89
|
-
default_query_parser.key_space_limit
|
90
|
-
end
|
91
|
-
|
92
|
-
def self.key_space_limit=(v)
|
93
|
-
self.default_query_parser = self.default_query_parser.new_space_limit(v)
|
94
|
-
end
|
95
|
-
|
96
89
|
if defined?(Process::CLOCK_MONOTONIC)
|
97
90
|
def clock_time
|
98
91
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
@@ -131,19 +124,19 @@ module Rack
|
|
131
124
|
}.join("&")
|
132
125
|
when Hash
|
133
126
|
value.map { |k, v|
|
134
|
-
build_nested_query(v, prefix ? "#{prefix}[#{
|
127
|
+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
135
128
|
}.delete_if(&:empty?).join('&')
|
136
129
|
when nil
|
137
|
-
prefix
|
130
|
+
escape(prefix)
|
138
131
|
else
|
139
132
|
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
140
|
-
"#{prefix}=#{escape(value)}"
|
133
|
+
"#{escape(prefix)}=#{escape(value)}"
|
141
134
|
end
|
142
135
|
end
|
143
136
|
|
144
137
|
def q_values(q_value_header)
|
145
|
-
q_value_header.to_s.split(
|
146
|
-
value, parameters = part.split(
|
138
|
+
q_value_header.to_s.split(',').map do |part|
|
139
|
+
value, parameters = part.split(';', 2).map(&:strip)
|
147
140
|
quality = 1.0
|
148
141
|
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
|
149
142
|
quality = md[1].to_f
|
@@ -152,6 +145,20 @@ module Rack
|
|
152
145
|
end
|
153
146
|
end
|
154
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
|
+
|
155
162
|
# Return best accept value to use, based on the algorithm
|
156
163
|
# in RFC 2616 Section 14. If there are multiple best
|
157
164
|
# matches (same specificity and quality), the value returned
|
@@ -166,23 +173,19 @@ module Rack
|
|
166
173
|
end.compact.sort_by do |match, quality|
|
167
174
|
(match.split('/', 2).count('*') * -10) + quality
|
168
175
|
end.last
|
169
|
-
matches
|
176
|
+
matches&.first
|
170
177
|
end
|
171
178
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
'
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
184
|
-
def escape_html(string)
|
185
|
-
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
|
179
|
+
# Introduced in ERB 4.0. ERB::Escape is an alias for ERB::Utils which
|
180
|
+
# doesn't get monkey-patched by rails
|
181
|
+
if defined?(ERB::Escape) && ERB::Escape.instance_method(:html_escape)
|
182
|
+
define_method(:escape_html, ERB::Escape.instance_method(:html_escape))
|
183
|
+
else
|
184
|
+
require 'cgi/escape'
|
185
|
+
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
186
|
+
def escape_html(string)
|
187
|
+
CGI.escapeHTML(string.to_s)
|
188
|
+
end
|
186
189
|
end
|
187
190
|
|
188
191
|
def select_best_encoding(available_encodings, accept_encoding)
|
@@ -217,145 +220,199 @@ module Rack
|
|
217
220
|
(encoding_candidates & available_encodings)[0]
|
218
221
|
end
|
219
222
|
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
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)
|
234
240
|
end
|
235
241
|
end
|
236
242
|
|
237
|
-
|
243
|
+
# :call-seq:
|
244
|
+
# parse_cookies(env) -> hash
|
245
|
+
#
|
246
|
+
# Parse cookies from the provided request environment using
|
247
|
+
# parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
|
248
|
+
#
|
249
|
+
# parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
|
250
|
+
# # => {'myname' => 'myvalue'}
|
251
|
+
#
|
252
|
+
def parse_cookies(env)
|
253
|
+
parse_cookies_header env[HTTP_COOKIE]
|
254
|
+
end
|
255
|
+
|
256
|
+
# A valid cookie key according to RFC2616.
|
257
|
+
# 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: ( ) < > @ , ; : \ " / [ ] ? = { }.
|
258
|
+
VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
|
259
|
+
private_constant :VALID_COOKIE_KEY
|
260
|
+
|
261
|
+
private def escape_cookie_key(key)
|
262
|
+
if key =~ VALID_COOKIE_KEY
|
263
|
+
key
|
264
|
+
else
|
265
|
+
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
|
266
|
+
escape(key)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# :call-seq:
|
271
|
+
# set_cookie_header(key, value) -> encoded string
|
272
|
+
#
|
273
|
+
# Generate an encoded string using the provided +key+ and +value+ suitable
|
274
|
+
# for the +set-cookie+ header according to RFC6265. The +value+ may be an
|
275
|
+
# instance of either +String+ or +Hash+.
|
276
|
+
#
|
277
|
+
# If the cookie +value+ is an instance of +Hash+, it considers the following
|
278
|
+
# cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
|
279
|
+
# of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
|
280
|
+
# details about the interpretation of these fields, consult
|
281
|
+
# [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
|
282
|
+
#
|
283
|
+
# An extra cookie attribute +escape_key+ can be provided to control whether
|
284
|
+
# or not the cookie key is URL encoded. If explicitly set to +false+, the
|
285
|
+
# cookie key name will not be url encoded (escaped). The default is +true+.
|
286
|
+
#
|
287
|
+
# set_cookie_header("myname", "myvalue")
|
288
|
+
# # => "myname=myvalue"
|
289
|
+
#
|
290
|
+
# set_cookie_header("myname", {value: "myvalue", max_age: 10})
|
291
|
+
# # => "myname=myvalue; max-age=10"
|
292
|
+
#
|
293
|
+
def set_cookie_header(key, value)
|
238
294
|
case value
|
239
295
|
when Hash
|
296
|
+
key = escape_cookie_key(key) unless value[:escape_key] == false
|
240
297
|
domain = "; domain=#{value[:domain]}" if value[:domain]
|
241
298
|
path = "; path=#{value[:path]}" if value[:path]
|
242
299
|
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
243
300
|
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
244
301
|
secure = "; secure" if value[:secure]
|
245
|
-
httponly = ";
|
302
|
+
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
246
303
|
same_site =
|
247
304
|
case value[:same_site]
|
248
305
|
when false, nil
|
249
306
|
nil
|
250
307
|
when :none, 'None', :None
|
251
|
-
';
|
308
|
+
'; samesite=none'
|
252
309
|
when :lax, 'Lax', :Lax
|
253
|
-
';
|
310
|
+
'; samesite=lax'
|
254
311
|
when true, :strict, 'Strict', :Strict
|
255
|
-
';
|
312
|
+
'; samesite=strict'
|
256
313
|
else
|
257
|
-
raise ArgumentError, "Invalid
|
314
|
+
raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
|
258
315
|
end
|
316
|
+
partitioned = "; partitioned" if value[:partitioned]
|
259
317
|
value = value[:value]
|
318
|
+
else
|
319
|
+
key = escape_cookie_key(key)
|
260
320
|
end
|
321
|
+
|
261
322
|
value = [value] unless Array === value
|
262
323
|
|
263
|
-
|
264
|
-
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
|
324
|
+
return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
|
325
|
+
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
|
326
|
+
end
|
265
327
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
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
|
-
|
344
|
+
headers[SET_COOKIE] = set_cookie_header(key, value)
|
275
345
|
end
|
276
346
|
end
|
277
347
|
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
-
def
|
284
|
-
|
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
|
307
|
-
|
308
|
-
cookies.reject! { |cookie| regexp.match? cookie }
|
366
|
+
def delete_cookie_header!(headers, key, value = {})
|
367
|
+
headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
|
309
368
|
|
310
|
-
|
369
|
+
return nil
|
311
370
|
end
|
312
371
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
#
|
319
|
-
#
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
372
|
+
# :call-seq:
|
373
|
+
# delete_set_cookie_header!(header, key, value = {}) -> header value
|
374
|
+
#
|
375
|
+
# Set an expired cookie in the specified headers with the given cookie
|
376
|
+
# +key+ and +value+ using delete_set_cookie_header. This causes
|
377
|
+
# the client to immediately delete the specified cookie.
|
378
|
+
#
|
379
|
+
# delete_set_cookie_header!(nil, "mycookie")
|
380
|
+
# # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
381
|
+
#
|
382
|
+
# If the header is non-nil, it will be modified in place.
|
383
|
+
#
|
384
|
+
# header = []
|
385
|
+
# delete_set_cookie_header!(header, "mycookie")
|
386
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
387
|
+
# header
|
388
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
389
|
+
#
|
390
|
+
def delete_set_cookie_header!(header, key, value = {})
|
391
|
+
if header
|
392
|
+
header = Array(header)
|
393
|
+
header << delete_set_cookie_header(key, value)
|
394
|
+
else
|
395
|
+
header = delete_set_cookie_header(key, value)
|
396
|
+
end
|
327
397
|
|
398
|
+
return header
|
328
399
|
end
|
329
400
|
|
330
401
|
def rfc2822(time)
|
331
402
|
time.rfc2822
|
332
403
|
end
|
333
404
|
|
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
405
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
350
406
|
# Returns nil if the header is missing or syntactically invalid.
|
351
407
|
# Returns an empty array if none of the ranges are satisfiable.
|
352
408
|
def byte_ranges(env, size)
|
353
|
-
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
|
354
409
|
get_byte_ranges env['HTTP_RANGE'], size
|
355
410
|
end
|
356
411
|
|
357
412
|
def get_byte_ranges(http_range, size)
|
358
413
|
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
414
|
+
# Ignore Range when file size is 0 to avoid a 416 error.
|
415
|
+
return nil if size.zero?
|
359
416
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
360
417
|
ranges = []
|
361
418
|
$1.split(/,\s*/).each do |range_spec|
|
@@ -380,23 +437,36 @@ module Rack
|
|
380
437
|
end
|
381
438
|
ranges << (r0..r1) if r0 <= r1
|
382
439
|
end
|
440
|
+
|
441
|
+
return [] if ranges.map(&:size).sum > size
|
442
|
+
|
383
443
|
ranges
|
384
444
|
end
|
385
445
|
|
386
|
-
#
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
446
|
+
# :nocov:
|
447
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
448
|
+
# Constant time string comparison.
|
449
|
+
#
|
450
|
+
# NOTE: the values compared should be of fixed length, such as strings
|
451
|
+
# that have already been processed by HMAC. This should not be used
|
452
|
+
# on variable length plaintext strings because it could leak length info
|
453
|
+
# via timing attacks.
|
454
|
+
def secure_compare(a, b)
|
455
|
+
return false unless a.bytesize == b.bytesize
|
394
456
|
|
395
|
-
|
457
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
458
|
+
end
|
459
|
+
# :nocov:
|
460
|
+
else
|
461
|
+
def secure_compare(a, b)
|
462
|
+
return false unless a.bytesize == b.bytesize
|
463
|
+
|
464
|
+
l = a.unpack("C*")
|
396
465
|
|
397
|
-
|
398
|
-
|
399
|
-
|
466
|
+
r, i = 0, -1
|
467
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
468
|
+
r == 0
|
469
|
+
end
|
400
470
|
end
|
401
471
|
|
402
472
|
# Context allows the use of a compatible middleware at different points
|
@@ -425,101 +495,12 @@ module Rack
|
|
425
495
|
end
|
426
496
|
end
|
427
497
|
|
428
|
-
# A case-insensitive Hash that preserves the original case of a
|
429
|
-
# header when set.
|
430
|
-
#
|
431
|
-
# @api private
|
432
|
-
class HeaderHash < Hash # :nodoc:
|
433
|
-
def self.[](headers)
|
434
|
-
if headers.is_a?(HeaderHash) && !headers.frozen?
|
435
|
-
return headers
|
436
|
-
else
|
437
|
-
return self.new(headers)
|
438
|
-
end
|
439
|
-
end
|
440
|
-
|
441
|
-
def initialize(hash = {})
|
442
|
-
super()
|
443
|
-
@names = {}
|
444
|
-
hash.each { |k, v| self[k] = v }
|
445
|
-
end
|
446
|
-
|
447
|
-
# on dup/clone, we need to duplicate @names hash
|
448
|
-
def initialize_copy(other)
|
449
|
-
super
|
450
|
-
@names = other.names.dup
|
451
|
-
end
|
452
|
-
|
453
|
-
# on clear, we need to clear @names hash
|
454
|
-
def clear
|
455
|
-
super
|
456
|
-
@names.clear
|
457
|
-
end
|
458
|
-
|
459
|
-
def each
|
460
|
-
super do |k, v|
|
461
|
-
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
462
|
-
end
|
463
|
-
end
|
464
|
-
|
465
|
-
def to_hash
|
466
|
-
hash = {}
|
467
|
-
each { |k, v| hash[k] = v }
|
468
|
-
hash
|
469
|
-
end
|
470
|
-
|
471
|
-
def [](k)
|
472
|
-
super(k) || super(@names[k.downcase])
|
473
|
-
end
|
474
|
-
|
475
|
-
def []=(k, v)
|
476
|
-
canonical = k.downcase.freeze
|
477
|
-
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
|
478
|
-
@names[canonical] = k
|
479
|
-
super k, v
|
480
|
-
end
|
481
|
-
|
482
|
-
def delete(k)
|
483
|
-
canonical = k.downcase
|
484
|
-
result = super @names.delete(canonical)
|
485
|
-
result
|
486
|
-
end
|
487
|
-
|
488
|
-
def include?(k)
|
489
|
-
super || @names.include?(k.downcase)
|
490
|
-
end
|
491
|
-
|
492
|
-
alias_method :has_key?, :include?
|
493
|
-
alias_method :member?, :include?
|
494
|
-
alias_method :key?, :include?
|
495
|
-
|
496
|
-
def merge!(other)
|
497
|
-
other.each { |k, v| self[k] = v }
|
498
|
-
self
|
499
|
-
end
|
500
|
-
|
501
|
-
def merge(other)
|
502
|
-
hash = dup
|
503
|
-
hash.merge! other
|
504
|
-
end
|
505
|
-
|
506
|
-
def replace(other)
|
507
|
-
clear
|
508
|
-
other.each { |k, v| self[k] = v }
|
509
|
-
self
|
510
|
-
end
|
511
|
-
|
512
|
-
protected
|
513
|
-
def names
|
514
|
-
@names
|
515
|
-
end
|
516
|
-
end
|
517
|
-
|
518
498
|
# Every standard HTTP code mapped to the appropriate message.
|
519
499
|
# Generated with:
|
520
|
-
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv
|
521
|
-
# ruby -
|
522
|
-
#
|
500
|
+
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
|
501
|
+
# | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
|
502
|
+
# .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
|
503
|
+
# .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
|
523
504
|
HTTP_STATUS_CODES = {
|
524
505
|
100 => 'Continue',
|
525
506
|
101 => 'Switching Protocols',
|
@@ -541,7 +522,6 @@ module Rack
|
|
541
522
|
303 => 'See Other',
|
542
523
|
304 => 'Not Modified',
|
543
524
|
305 => 'Use Proxy',
|
544
|
-
306 => '(Unused)',
|
545
525
|
307 => 'Temporary Redirect',
|
546
526
|
308 => 'Permanent Redirect',
|
547
527
|
400 => 'Bad Request',
|
@@ -557,13 +537,13 @@ module Rack
|
|
557
537
|
410 => 'Gone',
|
558
538
|
411 => 'Length Required',
|
559
539
|
412 => 'Precondition Failed',
|
560
|
-
413 => '
|
540
|
+
413 => 'Content Too Large',
|
561
541
|
414 => 'URI Too Long',
|
562
542
|
415 => 'Unsupported Media Type',
|
563
543
|
416 => 'Range Not Satisfiable',
|
564
544
|
417 => 'Expectation Failed',
|
565
545
|
421 => 'Misdirected Request',
|
566
|
-
422 => 'Unprocessable
|
546
|
+
422 => 'Unprocessable Content',
|
567
547
|
423 => 'Locked',
|
568
548
|
424 => 'Failed Dependency',
|
569
549
|
425 => 'Too Early',
|
@@ -571,7 +551,7 @@ module Rack
|
|
571
551
|
428 => 'Precondition Required',
|
572
552
|
429 => 'Too Many Requests',
|
573
553
|
431 => 'Request Header Fields Too Large',
|
574
|
-
451 => 'Unavailable
|
554
|
+
451 => 'Unavailable For Legal Reasons',
|
575
555
|
500 => 'Internal Server Error',
|
576
556
|
501 => 'Not Implemented',
|
577
557
|
502 => 'Bad Gateway',
|
@@ -581,8 +561,6 @@ module Rack
|
|
581
561
|
506 => 'Variant Also Negotiates',
|
582
562
|
507 => 'Insufficient Storage',
|
583
563
|
508 => 'Loop Detected',
|
584
|
-
509 => 'Bandwidth Limit Exceeded',
|
585
|
-
510 => 'Not Extended',
|
586
564
|
511 => 'Network Authentication Required'
|
587
565
|
}
|
588
566
|
|
@@ -590,12 +568,36 @@ module Rack
|
|
590
568
|
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
591
569
|
|
592
570
|
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
593
|
-
[message.downcase.gsub(/\s
|
571
|
+
[message.downcase.gsub(/\s|-/, '_').to_sym, code]
|
594
572
|
}.flatten]
|
595
573
|
|
574
|
+
OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
|
575
|
+
payload_too_large: 413,
|
576
|
+
unprocessable_entity: 422,
|
577
|
+
bandwidth_limit_exceeded: 509,
|
578
|
+
not_extended: 510
|
579
|
+
}.freeze
|
580
|
+
private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
|
581
|
+
|
582
|
+
OBSOLETE_SYMBOL_MAPPINGS = {
|
583
|
+
payload_too_large: :content_too_large,
|
584
|
+
unprocessable_entity: :unprocessable_content
|
585
|
+
}.freeze
|
586
|
+
private_constant :OBSOLETE_SYMBOL_MAPPINGS
|
587
|
+
|
596
588
|
def status_code(status)
|
597
589
|
if status.is_a?(Symbol)
|
598
|
-
SYMBOL_TO_STATUS_CODE.fetch(status)
|
590
|
+
SYMBOL_TO_STATUS_CODE.fetch(status) do
|
591
|
+
fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
|
592
|
+
message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
|
593
|
+
if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
|
594
|
+
# message = "#{message} Please use #{canonical_symbol.inspect} instead."
|
595
|
+
# For now, let's not emit any warning when there is a mapping.
|
596
|
+
else
|
597
|
+
warn message, uplevel: 3
|
598
|
+
end
|
599
|
+
fallback_code
|
600
|
+
end
|
599
601
|
else
|
600
602
|
status.to_i
|
601
603
|
end
|
data/lib/rack/version.rb
CHANGED
@@ -12,15 +12,7 @@
|
|
12
12
|
# so it should be enough just to <tt>require 'rack'</tt> in your code.
|
13
13
|
|
14
14
|
module Rack
|
15
|
-
|
16
|
-
VERSION = [1, 3]
|
17
|
-
|
18
|
-
# Return the Rack protocol version as a dotted string.
|
19
|
-
def self.version
|
20
|
-
VERSION.join(".")
|
21
|
-
end
|
22
|
-
|
23
|
-
RELEASE = "2.2.7"
|
15
|
+
RELEASE = "3.1.3"
|
24
16
|
|
25
17
|
# Return the Rack release as a dotted string.
|
26
18
|
def self.release
|