rack 2.2.10 → 3.1.10
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +346 -90
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +328 -0
- data/SPEC.rdoc +204 -131
- 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 -3
- 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 +25 -19
- 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 +840 -644
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/logger.rb +3 -0
- data/lib/rack/media_type.rb +8 -3
- 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 +161 -0
- data/lib/rack/mock_response.rb +124 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +213 -95
- 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 +260 -123
- data/lib/rack/response.rb +151 -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 +236 -237
- data/lib/rack/version.rb +1 -9
- data/lib/rack.rb +13 -89
- metadata +15 -44
- 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 -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/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,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,19 @@ 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
|
-
# 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
|
+
else
|
185
|
+
require 'cgi/escape'
|
186
|
+
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
187
|
+
def escape_html(string)
|
188
|
+
CGI.escapeHTML(string.to_s)
|
189
|
+
end
|
187
190
|
end
|
188
191
|
|
189
192
|
def select_best_encoding(available_encodings, accept_encoding)
|
@@ -218,145 +221,199 @@ module Rack
|
|
218
221
|
(encoding_candidates & available_encodings)[0]
|
219
222
|
end
|
220
223
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
+
# :call-seq:
|
225
|
+
# parse_cookies_header(value) -> hash
|
226
|
+
#
|
227
|
+
# Parse cookies from the provided header +value+ according to RFC6265. The
|
228
|
+
# syntax for cookie headers only supports semicolons. Returns a map of
|
229
|
+
# cookie +key+ to cookie +value+.
|
230
|
+
#
|
231
|
+
# parse_cookies_header('myname=myvalue; max-age=0')
|
232
|
+
# # => {"myname"=>"myvalue", "max-age"=>"0"}
|
233
|
+
#
|
234
|
+
def parse_cookies_header(value)
|
235
|
+
return {} unless value
|
224
236
|
|
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|
|
237
|
+
value.split(/; */n).each_with_object({}) do |cookie, cookies|
|
232
238
|
next if cookie.empty?
|
233
239
|
key, value = cookie.split('=', 2)
|
234
240
|
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
235
241
|
end
|
236
242
|
end
|
237
243
|
|
238
|
-
|
244
|
+
# :call-seq:
|
245
|
+
# parse_cookies(env) -> hash
|
246
|
+
#
|
247
|
+
# Parse cookies from the provided request environment using
|
248
|
+
# parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
|
249
|
+
#
|
250
|
+
# parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
|
251
|
+
# # => {'myname' => 'myvalue'}
|
252
|
+
#
|
253
|
+
def parse_cookies(env)
|
254
|
+
parse_cookies_header env[HTTP_COOKIE]
|
255
|
+
end
|
256
|
+
|
257
|
+
# A valid cookie key according to RFC2616.
|
258
|
+
# 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: ( ) < > @ , ; : \ " / [ ] ? = { }.
|
259
|
+
VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
|
260
|
+
private_constant :VALID_COOKIE_KEY
|
261
|
+
|
262
|
+
private def escape_cookie_key(key)
|
263
|
+
if key =~ VALID_COOKIE_KEY
|
264
|
+
key
|
265
|
+
else
|
266
|
+
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
|
267
|
+
escape(key)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# :call-seq:
|
272
|
+
# set_cookie_header(key, value) -> encoded string
|
273
|
+
#
|
274
|
+
# Generate an encoded string using the provided +key+ and +value+ suitable
|
275
|
+
# for the +set-cookie+ header according to RFC6265. The +value+ may be an
|
276
|
+
# instance of either +String+ or +Hash+.
|
277
|
+
#
|
278
|
+
# If the cookie +value+ is an instance of +Hash+, it considers the following
|
279
|
+
# cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
|
280
|
+
# of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
|
281
|
+
# details about the interpretation of these fields, consult
|
282
|
+
# [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
|
283
|
+
#
|
284
|
+
# An extra cookie attribute +escape_key+ can be provided to control whether
|
285
|
+
# or not the cookie key is URL encoded. If explicitly set to +false+, the
|
286
|
+
# cookie key name will not be url encoded (escaped). The default is +true+.
|
287
|
+
#
|
288
|
+
# set_cookie_header("myname", "myvalue")
|
289
|
+
# # => "myname=myvalue"
|
290
|
+
#
|
291
|
+
# set_cookie_header("myname", {value: "myvalue", max_age: 10})
|
292
|
+
# # => "myname=myvalue; max-age=10"
|
293
|
+
#
|
294
|
+
def set_cookie_header(key, value)
|
239
295
|
case value
|
240
296
|
when Hash
|
297
|
+
key = escape_cookie_key(key) unless value[:escape_key] == false
|
241
298
|
domain = "; domain=#{value[:domain]}" if value[:domain]
|
242
299
|
path = "; path=#{value[:path]}" if value[:path]
|
243
300
|
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
244
301
|
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
245
302
|
secure = "; secure" if value[:secure]
|
246
|
-
httponly = ";
|
303
|
+
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
247
304
|
same_site =
|
248
305
|
case value[:same_site]
|
249
306
|
when false, nil
|
250
307
|
nil
|
251
308
|
when :none, 'None', :None
|
252
|
-
';
|
309
|
+
'; samesite=none'
|
253
310
|
when :lax, 'Lax', :Lax
|
254
|
-
';
|
311
|
+
'; samesite=lax'
|
255
312
|
when true, :strict, 'Strict', :Strict
|
256
|
-
';
|
313
|
+
'; samesite=strict'
|
257
314
|
else
|
258
|
-
raise ArgumentError, "Invalid
|
315
|
+
raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
|
259
316
|
end
|
317
|
+
partitioned = "; partitioned" if value[:partitioned]
|
260
318
|
value = value[:value]
|
319
|
+
else
|
320
|
+
key = escape_cookie_key(key)
|
261
321
|
end
|
322
|
+
|
262
323
|
value = [value] unless Array === value
|
263
324
|
|
264
|
-
|
265
|
-
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
|
325
|
+
return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
|
326
|
+
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
|
327
|
+
end
|
266
328
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
329
|
+
# :call-seq:
|
330
|
+
# set_cookie_header!(headers, key, value) -> header value
|
331
|
+
#
|
332
|
+
# Append a cookie in the specified headers with the given cookie +key+ and
|
333
|
+
# +value+ using set_cookie_header.
|
334
|
+
#
|
335
|
+
# If the headers already contains a +set-cookie+ key, it will be converted
|
336
|
+
# to an +Array+ if not already, and appended to.
|
337
|
+
def set_cookie_header!(headers, key, value)
|
338
|
+
if header = headers[SET_COOKIE]
|
339
|
+
if header.is_a?(Array)
|
340
|
+
header << set_cookie_header(key, value)
|
341
|
+
else
|
342
|
+
headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
|
343
|
+
end
|
274
344
|
else
|
275
|
-
|
345
|
+
headers[SET_COOKIE] = set_cookie_header(key, value)
|
276
346
|
end
|
277
347
|
end
|
278
348
|
|
279
|
-
|
280
|
-
|
281
|
-
|
349
|
+
# :call-seq:
|
350
|
+
# delete_set_cookie_header(key, value = {}) -> encoded string
|
351
|
+
#
|
352
|
+
# Generate an encoded string based on the given +key+ and +value+ using
|
353
|
+
# set_cookie_header for the purpose of causing the specified cookie to be
|
354
|
+
# deleted. The +value+ may be an instance of +Hash+ and can include
|
355
|
+
# attributes as outlined by set_cookie_header. The encoded cookie will have
|
356
|
+
# a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
|
357
|
+
# +value+. When used with the +set-cookie+ header, it will cause the client
|
358
|
+
# to *remove* any matching cookie.
|
359
|
+
#
|
360
|
+
# delete_set_cookie_header("myname")
|
361
|
+
# # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
362
|
+
#
|
363
|
+
def delete_set_cookie_header(key, value = {})
|
364
|
+
set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
|
282
365
|
end
|
283
366
|
|
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 }
|
310
|
-
|
311
|
-
cookies.join("\n")
|
312
|
-
end
|
367
|
+
def delete_cookie_header!(headers, key, value = {})
|
368
|
+
headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
|
313
369
|
|
314
|
-
|
315
|
-
header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
|
316
|
-
nil
|
370
|
+
return nil
|
317
371
|
end
|
318
372
|
|
319
|
-
#
|
320
|
-
#
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
373
|
+
# :call-seq:
|
374
|
+
# delete_set_cookie_header!(header, key, value = {}) -> header value
|
375
|
+
#
|
376
|
+
# Set an expired cookie in the specified headers with the given cookie
|
377
|
+
# +key+ and +value+ using delete_set_cookie_header. This causes
|
378
|
+
# the client to immediately delete the specified cookie.
|
379
|
+
#
|
380
|
+
# delete_set_cookie_header!(nil, "mycookie")
|
381
|
+
# # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
382
|
+
#
|
383
|
+
# If the header is non-nil, it will be modified in place.
|
384
|
+
#
|
385
|
+
# header = []
|
386
|
+
# delete_set_cookie_header!(header, "mycookie")
|
387
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
388
|
+
# header
|
389
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
390
|
+
#
|
391
|
+
def delete_set_cookie_header!(header, key, value = {})
|
392
|
+
if header
|
393
|
+
header = Array(header)
|
394
|
+
header << delete_set_cookie_header(key, value)
|
395
|
+
else
|
396
|
+
header = delete_set_cookie_header(key, value)
|
397
|
+
end
|
328
398
|
|
399
|
+
return header
|
329
400
|
end
|
330
401
|
|
331
402
|
def rfc2822(time)
|
332
403
|
time.rfc2822
|
333
404
|
end
|
334
405
|
|
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
406
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
351
407
|
# Returns nil if the header is missing or syntactically invalid.
|
352
408
|
# Returns an empty array if none of the ranges are satisfiable.
|
353
409
|
def byte_ranges(env, size)
|
354
|
-
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
|
355
410
|
get_byte_ranges env['HTTP_RANGE'], size
|
356
411
|
end
|
357
412
|
|
358
413
|
def get_byte_ranges(http_range, size)
|
359
414
|
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
415
|
+
# Ignore Range when file size is 0 to avoid a 416 error.
|
416
|
+
return nil if size.zero?
|
360
417
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
361
418
|
ranges = []
|
362
419
|
$1.split(/,\s*/).each do |range_spec|
|
@@ -382,25 +439,35 @@ module Rack
|
|
382
439
|
ranges << (r0..r1) if r0 <= r1
|
383
440
|
end
|
384
441
|
|
385
|
-
return [] if ranges.map(&:size).
|
442
|
+
return [] if ranges.map(&:size).sum > size
|
386
443
|
|
387
444
|
ranges
|
388
445
|
end
|
389
446
|
|
390
|
-
#
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
447
|
+
# :nocov:
|
448
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
449
|
+
# Constant time string comparison.
|
450
|
+
#
|
451
|
+
# NOTE: the values compared should be of fixed length, such as strings
|
452
|
+
# that have already been processed by HMAC. This should not be used
|
453
|
+
# on variable length plaintext strings because it could leak length info
|
454
|
+
# via timing attacks.
|
455
|
+
def secure_compare(a, b)
|
456
|
+
return false unless a.bytesize == b.bytesize
|
457
|
+
|
458
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
459
|
+
end
|
460
|
+
# :nocov:
|
461
|
+
else
|
462
|
+
def secure_compare(a, b)
|
463
|
+
return false unless a.bytesize == b.bytesize
|
398
464
|
|
399
|
-
|
465
|
+
l = a.unpack("C*")
|
400
466
|
|
401
|
-
|
402
|
-
|
403
|
-
|
467
|
+
r, i = 0, -1
|
468
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
469
|
+
r == 0
|
470
|
+
end
|
404
471
|
end
|
405
472
|
|
406
473
|
# Context allows the use of a compatible middleware at different points
|
@@ -429,101 +496,12 @@ module Rack
|
|
429
496
|
end
|
430
497
|
end
|
431
498
|
|
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
499
|
# Every standard HTTP code mapped to the appropriate message.
|
523
500
|
# Generated with:
|
524
|
-
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv
|
525
|
-
# ruby -
|
526
|
-
#
|
501
|
+
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
|
502
|
+
# | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
|
503
|
+
# .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
|
504
|
+
# .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
|
527
505
|
HTTP_STATUS_CODES = {
|
528
506
|
100 => 'Continue',
|
529
507
|
101 => 'Switching Protocols',
|
@@ -545,7 +523,6 @@ module Rack
|
|
545
523
|
303 => 'See Other',
|
546
524
|
304 => 'Not Modified',
|
547
525
|
305 => 'Use Proxy',
|
548
|
-
306 => '(Unused)',
|
549
526
|
307 => 'Temporary Redirect',
|
550
527
|
308 => 'Permanent Redirect',
|
551
528
|
400 => 'Bad Request',
|
@@ -561,13 +538,13 @@ module Rack
|
|
561
538
|
410 => 'Gone',
|
562
539
|
411 => 'Length Required',
|
563
540
|
412 => 'Precondition Failed',
|
564
|
-
413 => '
|
541
|
+
413 => 'Content Too Large',
|
565
542
|
414 => 'URI Too Long',
|
566
543
|
415 => 'Unsupported Media Type',
|
567
544
|
416 => 'Range Not Satisfiable',
|
568
545
|
417 => 'Expectation Failed',
|
569
546
|
421 => 'Misdirected Request',
|
570
|
-
422 => 'Unprocessable
|
547
|
+
422 => 'Unprocessable Content',
|
571
548
|
423 => 'Locked',
|
572
549
|
424 => 'Failed Dependency',
|
573
550
|
425 => 'Too Early',
|
@@ -575,7 +552,7 @@ module Rack
|
|
575
552
|
428 => 'Precondition Required',
|
576
553
|
429 => 'Too Many Requests',
|
577
554
|
431 => 'Request Header Fields Too Large',
|
578
|
-
451 => 'Unavailable
|
555
|
+
451 => 'Unavailable For Legal Reasons',
|
579
556
|
500 => 'Internal Server Error',
|
580
557
|
501 => 'Not Implemented',
|
581
558
|
502 => 'Bad Gateway',
|
@@ -585,8 +562,6 @@ module Rack
|
|
585
562
|
506 => 'Variant Also Negotiates',
|
586
563
|
507 => 'Insufficient Storage',
|
587
564
|
508 => 'Loop Detected',
|
588
|
-
509 => 'Bandwidth Limit Exceeded',
|
589
|
-
510 => 'Not Extended',
|
590
565
|
511 => 'Network Authentication Required'
|
591
566
|
}
|
592
567
|
|
@@ -594,12 +569,36 @@ module Rack
|
|
594
569
|
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
595
570
|
|
596
571
|
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
597
|
-
[message.downcase.gsub(/\s
|
572
|
+
[message.downcase.gsub(/\s|-/, '_').to_sym, code]
|
598
573
|
}.flatten]
|
599
574
|
|
575
|
+
OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
|
576
|
+
payload_too_large: 413,
|
577
|
+
unprocessable_entity: 422,
|
578
|
+
bandwidth_limit_exceeded: 509,
|
579
|
+
not_extended: 510
|
580
|
+
}.freeze
|
581
|
+
private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
|
582
|
+
|
583
|
+
OBSOLETE_SYMBOL_MAPPINGS = {
|
584
|
+
payload_too_large: :content_too_large,
|
585
|
+
unprocessable_entity: :unprocessable_content
|
586
|
+
}.freeze
|
587
|
+
private_constant :OBSOLETE_SYMBOL_MAPPINGS
|
588
|
+
|
600
589
|
def status_code(status)
|
601
590
|
if status.is_a?(Symbol)
|
602
|
-
SYMBOL_TO_STATUS_CODE.fetch(status)
|
591
|
+
SYMBOL_TO_STATUS_CODE.fetch(status) do
|
592
|
+
fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
|
593
|
+
message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
|
594
|
+
if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
|
595
|
+
# message = "#{message} Please use #{canonical_symbol.inspect} instead."
|
596
|
+
# For now, let's not emit any warning when there is a mapping.
|
597
|
+
else
|
598
|
+
warn message, uplevel: 3
|
599
|
+
end
|
600
|
+
fallback_code
|
601
|
+
end
|
603
602
|
else
|
604
603
|
status.to_i
|
605
604
|
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.10"
|
15
|
+
RELEASE = "3.1.10"
|
24
16
|
|
25
17
|
# Return the Rack release as a dotted string.
|
26
18
|
def self.release
|