rack 2.2.8 → 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 +353 -81
- 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 -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 +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 +17 -7
- 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 +214 -90
- 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 +240 -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 -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 -204
- 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,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
|
+
URI_PARSER = defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER
|
27
28
|
|
28
29
|
class << self
|
29
30
|
attr_accessor :default_query_parser
|
30
31
|
end
|
31
|
-
# The default
|
32
|
-
# This helps prevent a rogue client from
|
33
|
-
|
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)
|
34
36
|
|
35
37
|
module_function
|
36
38
|
|
@@ -42,13 +44,13 @@ module Rack
|
|
42
44
|
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
|
43
45
|
# true URI escaping.
|
44
46
|
def escape_path(s)
|
45
|
-
|
47
|
+
URI_PARSER.escape s
|
46
48
|
end
|
47
49
|
|
48
50
|
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
|
49
51
|
# unescaping query parameters or form components.
|
50
52
|
def unescape_path(s)
|
51
|
-
|
53
|
+
URI_PARSER.unescape s
|
52
54
|
end
|
53
55
|
|
54
56
|
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
|
@@ -85,14 +87,6 @@ module Rack
|
|
85
87
|
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
|
86
88
|
end
|
87
89
|
|
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
90
|
if defined?(Process::CLOCK_MONOTONIC)
|
97
91
|
def clock_time
|
98
92
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
@@ -131,19 +125,19 @@ module Rack
|
|
131
125
|
}.join("&")
|
132
126
|
when Hash
|
133
127
|
value.map { |k, v|
|
134
|
-
build_nested_query(v, prefix ? "#{prefix}[#{
|
128
|
+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
135
129
|
}.delete_if(&:empty?).join('&')
|
136
130
|
when nil
|
137
|
-
prefix
|
131
|
+
escape(prefix)
|
138
132
|
else
|
139
133
|
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
140
|
-
"#{prefix}=#{escape(value)}"
|
134
|
+
"#{escape(prefix)}=#{escape(value)}"
|
141
135
|
end
|
142
136
|
end
|
143
137
|
|
144
138
|
def q_values(q_value_header)
|
145
|
-
q_value_header.to_s.split(
|
146
|
-
value, parameters = part.split(
|
139
|
+
q_value_header.to_s.split(',').map do |part|
|
140
|
+
value, parameters = part.split(';', 2).map(&:strip)
|
147
141
|
quality = 1.0
|
148
142
|
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
|
149
143
|
quality = md[1].to_f
|
@@ -152,6 +146,20 @@ module Rack
|
|
152
146
|
end
|
153
147
|
end
|
154
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
|
+
|
155
163
|
# Return best accept value to use, based on the algorithm
|
156
164
|
# in RFC 2616 Section 14. If there are multiple best
|
157
165
|
# matches (same specificity and quality), the value returned
|
@@ -166,23 +174,19 @@ module Rack
|
|
166
174
|
end.compact.sort_by do |match, quality|
|
167
175
|
(match.split('/', 2).count('*') * -10) + quality
|
168
176
|
end.last
|
169
|
-
matches
|
177
|
+
matches&.first
|
170
178
|
end
|
171
179
|
|
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] }
|
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
|
186
190
|
end
|
187
191
|
|
188
192
|
def select_best_encoding(available_encodings, accept_encoding)
|
@@ -217,145 +221,199 @@ module Rack
|
|
217
221
|
(encoding_candidates & available_encodings)[0]
|
218
222
|
end
|
219
223
|
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
223
236
|
|
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|
|
237
|
+
value.split(/; */n).each_with_object({}) do |cookie, cookies|
|
231
238
|
next if cookie.empty?
|
232
239
|
key, value = cookie.split('=', 2)
|
233
240
|
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
234
241
|
end
|
235
242
|
end
|
236
243
|
|
237
|
-
|
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)
|
238
295
|
case value
|
239
296
|
when Hash
|
297
|
+
key = escape_cookie_key(key) unless value[:escape_key] == false
|
240
298
|
domain = "; domain=#{value[:domain]}" if value[:domain]
|
241
299
|
path = "; path=#{value[:path]}" if value[:path]
|
242
300
|
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
243
301
|
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
244
302
|
secure = "; secure" if value[:secure]
|
245
|
-
httponly = ";
|
303
|
+
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
246
304
|
same_site =
|
247
305
|
case value[:same_site]
|
248
306
|
when false, nil
|
249
307
|
nil
|
250
308
|
when :none, 'None', :None
|
251
|
-
';
|
309
|
+
'; samesite=none'
|
252
310
|
when :lax, 'Lax', :Lax
|
253
|
-
';
|
311
|
+
'; samesite=lax'
|
254
312
|
when true, :strict, 'Strict', :Strict
|
255
|
-
';
|
313
|
+
'; samesite=strict'
|
256
314
|
else
|
257
|
-
raise ArgumentError, "Invalid
|
315
|
+
raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
|
258
316
|
end
|
317
|
+
partitioned = "; partitioned" if value[:partitioned]
|
259
318
|
value = value[:value]
|
319
|
+
else
|
320
|
+
key = escape_cookie_key(key)
|
260
321
|
end
|
322
|
+
|
261
323
|
value = [value] unless Array === value
|
262
324
|
|
263
|
-
|
264
|
-
"#{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
|
265
328
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
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
|
273
344
|
else
|
274
|
-
|
345
|
+
headers[SET_COOKIE] = set_cookie_header(key, value)
|
275
346
|
end
|
276
347
|
end
|
277
348
|
|
278
|
-
|
279
|
-
|
280
|
-
|
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: ''))
|
281
365
|
end
|
282
366
|
|
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 }
|
367
|
+
def delete_cookie_header!(headers, key, value = {})
|
368
|
+
headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
|
309
369
|
|
310
|
-
|
370
|
+
return nil
|
311
371
|
end
|
312
372
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
#
|
319
|
-
#
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
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
|
327
398
|
|
399
|
+
return header
|
328
400
|
end
|
329
401
|
|
330
402
|
def rfc2822(time)
|
331
403
|
time.rfc2822
|
332
404
|
end
|
333
405
|
|
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
406
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
350
407
|
# Returns nil if the header is missing or syntactically invalid.
|
351
408
|
# Returns an empty array if none of the ranges are satisfiable.
|
352
409
|
def byte_ranges(env, size)
|
353
|
-
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
|
354
410
|
get_byte_ranges env['HTTP_RANGE'], size
|
355
411
|
end
|
356
412
|
|
357
413
|
def get_byte_ranges(http_range, size)
|
358
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?
|
359
417
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
360
418
|
ranges = []
|
361
419
|
$1.split(/,\s*/).each do |range_spec|
|
@@ -380,23 +438,36 @@ module Rack
|
|
380
438
|
end
|
381
439
|
ranges << (r0..r1) if r0 <= r1
|
382
440
|
end
|
441
|
+
|
442
|
+
return [] if ranges.map(&:size).sum > size
|
443
|
+
|
383
444
|
ranges
|
384
445
|
end
|
385
446
|
|
386
|
-
#
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
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
|
394
457
|
|
395
|
-
|
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
|
464
|
+
|
465
|
+
l = a.unpack("C*")
|
396
466
|
|
397
|
-
|
398
|
-
|
399
|
-
|
467
|
+
r, i = 0, -1
|
468
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
469
|
+
r == 0
|
470
|
+
end
|
400
471
|
end
|
401
472
|
|
402
473
|
# Context allows the use of a compatible middleware at different points
|
@@ -425,101 +496,12 @@ module Rack
|
|
425
496
|
end
|
426
497
|
end
|
427
498
|
|
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
499
|
# Every standard HTTP code mapped to the appropriate message.
|
519
500
|
# Generated with:
|
520
|
-
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv
|
521
|
-
# ruby -
|
522
|
-
#
|
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)"
|
523
505
|
HTTP_STATUS_CODES = {
|
524
506
|
100 => 'Continue',
|
525
507
|
101 => 'Switching Protocols',
|
@@ -541,7 +523,6 @@ module Rack
|
|
541
523
|
303 => 'See Other',
|
542
524
|
304 => 'Not Modified',
|
543
525
|
305 => 'Use Proxy',
|
544
|
-
306 => '(Unused)',
|
545
526
|
307 => 'Temporary Redirect',
|
546
527
|
308 => 'Permanent Redirect',
|
547
528
|
400 => 'Bad Request',
|
@@ -557,13 +538,13 @@ module Rack
|
|
557
538
|
410 => 'Gone',
|
558
539
|
411 => 'Length Required',
|
559
540
|
412 => 'Precondition Failed',
|
560
|
-
413 => '
|
541
|
+
413 => 'Content Too Large',
|
561
542
|
414 => 'URI Too Long',
|
562
543
|
415 => 'Unsupported Media Type',
|
563
544
|
416 => 'Range Not Satisfiable',
|
564
545
|
417 => 'Expectation Failed',
|
565
546
|
421 => 'Misdirected Request',
|
566
|
-
422 => 'Unprocessable
|
547
|
+
422 => 'Unprocessable Content',
|
567
548
|
423 => 'Locked',
|
568
549
|
424 => 'Failed Dependency',
|
569
550
|
425 => 'Too Early',
|
@@ -571,7 +552,7 @@ module Rack
|
|
571
552
|
428 => 'Precondition Required',
|
572
553
|
429 => 'Too Many Requests',
|
573
554
|
431 => 'Request Header Fields Too Large',
|
574
|
-
451 => 'Unavailable
|
555
|
+
451 => 'Unavailable For Legal Reasons',
|
575
556
|
500 => 'Internal Server Error',
|
576
557
|
501 => 'Not Implemented',
|
577
558
|
502 => 'Bad Gateway',
|
@@ -581,8 +562,6 @@ module Rack
|
|
581
562
|
506 => 'Variant Also Negotiates',
|
582
563
|
507 => 'Insufficient Storage',
|
583
564
|
508 => 'Loop Detected',
|
584
|
-
509 => 'Bandwidth Limit Exceeded',
|
585
|
-
510 => 'Not Extended',
|
586
565
|
511 => 'Network Authentication Required'
|
587
566
|
}
|
588
567
|
|
@@ -590,12 +569,36 @@ module Rack
|
|
590
569
|
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
591
570
|
|
592
571
|
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
593
|
-
[message.downcase.gsub(/\s
|
572
|
+
[message.downcase.gsub(/\s|-/, '_').to_sym, code]
|
594
573
|
}.flatten]
|
595
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
|
+
|
596
589
|
def status_code(status)
|
597
590
|
if status.is_a?(Symbol)
|
598
|
-
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
|
599
602
|
else
|
600
603
|
status.to_i
|
601
604
|
end
|