rack 2.2.6 → 3.1.2
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 +295 -72
- 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 +232 -94
- 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 +246 -121
- 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 +247 -244
- data/lib/rack/version.rb +1 -9
- data/lib/rack.rb +13 -89
- metadata +15 -41
- data/README.rdoc +0 -306
- 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 'cgi/escape'
|
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
|
|
@@ -58,13 +59,24 @@ module Rack
|
|
58
59
|
end
|
59
60
|
|
60
61
|
class << self
|
61
|
-
attr_accessor :
|
62
|
+
attr_accessor :multipart_total_part_limit
|
63
|
+
|
64
|
+
attr_accessor :multipart_file_limit
|
65
|
+
|
66
|
+
# multipart_part_limit is the original name of multipart_file_limit, but
|
67
|
+
# the limit only counts parts with filenames.
|
68
|
+
alias multipart_part_limit multipart_file_limit
|
69
|
+
alias multipart_part_limit= multipart_file_limit=
|
62
70
|
end
|
63
71
|
|
64
|
-
# The maximum number of parts a request can contain. Accepting too
|
65
|
-
# can lead to the server running out of file handles.
|
72
|
+
# The maximum number of file parts a request can contain. Accepting too
|
73
|
+
# many parts can lead to the server running out of file handles.
|
66
74
|
# Set to `0` for no limit.
|
67
|
-
self.
|
75
|
+
self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
|
76
|
+
|
77
|
+
# The maximum total number of parts a request can contain. Accepting too
|
78
|
+
# many can lead to excessive memory use and parsing time.
|
79
|
+
self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
|
68
80
|
|
69
81
|
def self.param_depth_limit
|
70
82
|
default_query_parser.param_depth_limit
|
@@ -74,14 +86,6 @@ module Rack
|
|
74
86
|
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
|
75
87
|
end
|
76
88
|
|
77
|
-
def self.key_space_limit
|
78
|
-
default_query_parser.key_space_limit
|
79
|
-
end
|
80
|
-
|
81
|
-
def self.key_space_limit=(v)
|
82
|
-
self.default_query_parser = self.default_query_parser.new_space_limit(v)
|
83
|
-
end
|
84
|
-
|
85
89
|
if defined?(Process::CLOCK_MONOTONIC)
|
86
90
|
def clock_time
|
87
91
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
@@ -120,19 +124,19 @@ module Rack
|
|
120
124
|
}.join("&")
|
121
125
|
when Hash
|
122
126
|
value.map { |k, v|
|
123
|
-
build_nested_query(v, prefix ? "#{prefix}[#{
|
127
|
+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
124
128
|
}.delete_if(&:empty?).join('&')
|
125
129
|
when nil
|
126
|
-
prefix
|
130
|
+
escape(prefix)
|
127
131
|
else
|
128
132
|
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
129
|
-
"#{prefix}=#{escape(value)}"
|
133
|
+
"#{escape(prefix)}=#{escape(value)}"
|
130
134
|
end
|
131
135
|
end
|
132
136
|
|
133
137
|
def q_values(q_value_header)
|
134
|
-
q_value_header.to_s.split(
|
135
|
-
value, parameters = part.split(
|
138
|
+
q_value_header.to_s.split(',').map do |part|
|
139
|
+
value, parameters = part.split(';', 2).map(&:strip)
|
136
140
|
quality = 1.0
|
137
141
|
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
|
138
142
|
quality = md[1].to_f
|
@@ -141,6 +145,20 @@ module Rack
|
|
141
145
|
end
|
142
146
|
end
|
143
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
|
+
|
144
162
|
# Return best accept value to use, based on the algorithm
|
145
163
|
# in RFC 2616 Section 14. If there are multiple best
|
146
164
|
# matches (same specificity and quality), the value returned
|
@@ -155,24 +173,11 @@ module Rack
|
|
155
173
|
end.compact.sort_by do |match, quality|
|
156
174
|
(match.split('/', 2).count('*') * -10) + quality
|
157
175
|
end.last
|
158
|
-
matches
|
176
|
+
matches&.first
|
159
177
|
end
|
160
178
|
|
161
|
-
ESCAPE_HTML = {
|
162
|
-
"&" => "&",
|
163
|
-
"<" => "<",
|
164
|
-
">" => ">",
|
165
|
-
"'" => "'",
|
166
|
-
'"' => """,
|
167
|
-
"/" => "/"
|
168
|
-
}
|
169
|
-
|
170
|
-
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
|
171
|
-
|
172
179
|
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
173
|
-
|
174
|
-
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
|
175
|
-
end
|
180
|
+
define_method(:escape_html, CGI.method(:escapeHTML))
|
176
181
|
|
177
182
|
def select_best_encoding(available_encodings, accept_encoding)
|
178
183
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
@@ -206,159 +211,214 @@ module Rack
|
|
206
211
|
(encoding_candidates & available_encodings)[0]
|
207
212
|
end
|
208
213
|
|
209
|
-
|
210
|
-
|
211
|
-
|
214
|
+
# :call-seq:
|
215
|
+
# parse_cookies_header(value) -> hash
|
216
|
+
#
|
217
|
+
# Parse cookies from the provided header +value+ according to RFC6265. The
|
218
|
+
# syntax for cookie headers only supports semicolons. Returns a map of
|
219
|
+
# cookie +key+ to cookie +value+.
|
220
|
+
#
|
221
|
+
# parse_cookies_header('myname=myvalue; max-age=0')
|
222
|
+
# # => {"myname"=>"myvalue", "max-age"=>"0"}
|
223
|
+
#
|
224
|
+
def parse_cookies_header(value)
|
225
|
+
return {} unless value
|
212
226
|
|
213
|
-
|
214
|
-
# According to RFC 6265:
|
215
|
-
# The syntax for cookie headers only supports semicolons
|
216
|
-
# User Agent -> Server ==
|
217
|
-
# Cookie: SID=31d4d96e407aad42; lang=en-US
|
218
|
-
return {} unless header
|
219
|
-
header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
|
227
|
+
value.split(/; */n).each_with_object({}) do |cookie, cookies|
|
220
228
|
next if cookie.empty?
|
221
229
|
key, value = cookie.split('=', 2)
|
222
230
|
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
223
231
|
end
|
224
232
|
end
|
225
233
|
|
226
|
-
|
234
|
+
# :call-seq:
|
235
|
+
# parse_cookies(env) -> hash
|
236
|
+
#
|
237
|
+
# Parse cookies from the provided request environment using
|
238
|
+
# parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
|
239
|
+
#
|
240
|
+
# parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
|
241
|
+
# # => {'myname' => 'myvalue'}
|
242
|
+
#
|
243
|
+
def parse_cookies(env)
|
244
|
+
parse_cookies_header env[HTTP_COOKIE]
|
245
|
+
end
|
246
|
+
|
247
|
+
# A valid cookie key according to RFC2616.
|
248
|
+
# A <cookie-name> can be any US-ASCII characters, except control characters, spaces, or tabs. It also must not contain a separator character like the following: ( ) < > @ , ; : \ " / [ ] ? = { }.
|
249
|
+
VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
|
250
|
+
private_constant :VALID_COOKIE_KEY
|
251
|
+
|
252
|
+
private def escape_cookie_key(key)
|
253
|
+
if key =~ VALID_COOKIE_KEY
|
254
|
+
key
|
255
|
+
else
|
256
|
+
warn "Cookie key #{key.inspect} is not valid according to RFC2616; it will be escaped. This behaviour is deprecated and will be removed in a future version of Rack.", uplevel: 2
|
257
|
+
escape(key)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# :call-seq:
|
262
|
+
# set_cookie_header(key, value) -> encoded string
|
263
|
+
#
|
264
|
+
# Generate an encoded string using the provided +key+ and +value+ suitable
|
265
|
+
# for the +set-cookie+ header according to RFC6265. The +value+ may be an
|
266
|
+
# instance of either +String+ or +Hash+.
|
267
|
+
#
|
268
|
+
# If the cookie +value+ is an instance of +Hash+, it considers the following
|
269
|
+
# cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
|
270
|
+
# of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
|
271
|
+
# details about the interpretation of these fields, consult
|
272
|
+
# [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
|
273
|
+
#
|
274
|
+
# An extra cookie attribute +escape_key+ can be provided to control whether
|
275
|
+
# or not the cookie key is URL encoded. If explicitly set to +false+, the
|
276
|
+
# cookie key name will not be url encoded (escaped). The default is +true+.
|
277
|
+
#
|
278
|
+
# set_cookie_header("myname", "myvalue")
|
279
|
+
# # => "myname=myvalue"
|
280
|
+
#
|
281
|
+
# set_cookie_header("myname", {value: "myvalue", max_age: 10})
|
282
|
+
# # => "myname=myvalue; max-age=10"
|
283
|
+
#
|
284
|
+
def set_cookie_header(key, value)
|
227
285
|
case value
|
228
286
|
when Hash
|
287
|
+
key = escape_cookie_key(key) unless value[:escape_key] == false
|
229
288
|
domain = "; domain=#{value[:domain]}" if value[:domain]
|
230
289
|
path = "; path=#{value[:path]}" if value[:path]
|
231
290
|
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
232
291
|
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
233
292
|
secure = "; secure" if value[:secure]
|
234
|
-
httponly = ";
|
293
|
+
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
235
294
|
same_site =
|
236
295
|
case value[:same_site]
|
237
296
|
when false, nil
|
238
297
|
nil
|
239
298
|
when :none, 'None', :None
|
240
|
-
';
|
299
|
+
'; samesite=none'
|
241
300
|
when :lax, 'Lax', :Lax
|
242
|
-
';
|
301
|
+
'; samesite=lax'
|
243
302
|
when true, :strict, 'Strict', :Strict
|
244
|
-
';
|
303
|
+
'; samesite=strict'
|
245
304
|
else
|
246
|
-
raise ArgumentError, "Invalid
|
305
|
+
raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
|
247
306
|
end
|
307
|
+
partitioned = "; partitioned" if value[:partitioned]
|
248
308
|
value = value[:value]
|
309
|
+
else
|
310
|
+
key = escape_cookie_key(key)
|
249
311
|
end
|
312
|
+
|
250
313
|
value = [value] unless Array === value
|
251
314
|
|
252
|
-
|
253
|
-
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
|
315
|
+
return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
|
316
|
+
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
|
317
|
+
end
|
254
318
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
319
|
+
# :call-seq:
|
320
|
+
# set_cookie_header!(headers, key, value) -> header value
|
321
|
+
#
|
322
|
+
# Append a cookie in the specified headers with the given cookie +key+ and
|
323
|
+
# +value+ using set_cookie_header.
|
324
|
+
#
|
325
|
+
# If the headers already contains a +set-cookie+ key, it will be converted
|
326
|
+
# to an +Array+ if not already, and appended to.
|
327
|
+
def set_cookie_header!(headers, key, value)
|
328
|
+
if header = headers[SET_COOKIE]
|
329
|
+
if header.is_a?(Array)
|
330
|
+
header << set_cookie_header(key, value)
|
331
|
+
else
|
332
|
+
headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
|
333
|
+
end
|
262
334
|
else
|
263
|
-
|
335
|
+
headers[SET_COOKIE] = set_cookie_header(key, value)
|
264
336
|
end
|
265
337
|
end
|
266
338
|
|
267
|
-
|
268
|
-
|
269
|
-
|
339
|
+
# :call-seq:
|
340
|
+
# delete_set_cookie_header(key, value = {}) -> encoded string
|
341
|
+
#
|
342
|
+
# Generate an encoded string based on the given +key+ and +value+ using
|
343
|
+
# set_cookie_header for the purpose of causing the specified cookie to be
|
344
|
+
# deleted. The +value+ may be an instance of +Hash+ and can include
|
345
|
+
# attributes as outlined by set_cookie_header. The encoded cookie will have
|
346
|
+
# a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
|
347
|
+
# +value+. When used with the +set-cookie+ header, it will cause the client
|
348
|
+
# to *remove* any matching cookie.
|
349
|
+
#
|
350
|
+
# delete_set_cookie_header("myname")
|
351
|
+
# # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
352
|
+
#
|
353
|
+
def delete_set_cookie_header(key, value = {})
|
354
|
+
set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
|
270
355
|
end
|
271
356
|
|
272
|
-
def
|
273
|
-
|
274
|
-
when nil, ''
|
275
|
-
cookies = []
|
276
|
-
when String
|
277
|
-
cookies = header.split("\n")
|
278
|
-
when Array
|
279
|
-
cookies = header
|
280
|
-
end
|
281
|
-
|
282
|
-
key = escape(key)
|
283
|
-
domain = value[:domain]
|
284
|
-
path = value[:path]
|
285
|
-
regexp = if domain
|
286
|
-
if path
|
287
|
-
/\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
|
288
|
-
else
|
289
|
-
/\A#{key}=.*domain=#{domain}(?:;|$)/
|
290
|
-
end
|
291
|
-
elsif path
|
292
|
-
/\A#{key}=.*path=#{path}(?:;|$)/
|
293
|
-
else
|
294
|
-
/\A#{key}=/
|
295
|
-
end
|
296
|
-
|
297
|
-
cookies.reject! { |cookie| regexp.match? cookie }
|
298
|
-
|
299
|
-
cookies.join("\n")
|
300
|
-
end
|
357
|
+
def delete_cookie_header!(headers, key, value = {})
|
358
|
+
headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
|
301
359
|
|
302
|
-
|
303
|
-
header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
|
304
|
-
nil
|
360
|
+
return nil
|
305
361
|
end
|
306
362
|
|
307
|
-
#
|
308
|
-
#
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
363
|
+
# :call-seq:
|
364
|
+
# delete_set_cookie_header!(header, key, value = {}) -> header value
|
365
|
+
#
|
366
|
+
# Set an expired cookie in the specified headers with the given cookie
|
367
|
+
# +key+ and +value+ using delete_set_cookie_header. This causes
|
368
|
+
# the client to immediately delete the specified cookie.
|
369
|
+
#
|
370
|
+
# delete_set_cookie_header!(nil, "mycookie")
|
371
|
+
# # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
372
|
+
#
|
373
|
+
# If the header is non-nil, it will be modified in place.
|
374
|
+
#
|
375
|
+
# header = []
|
376
|
+
# delete_set_cookie_header!(header, "mycookie")
|
377
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
378
|
+
# header
|
379
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
380
|
+
#
|
381
|
+
def delete_set_cookie_header!(header, key, value = {})
|
382
|
+
if header
|
383
|
+
header = Array(header)
|
384
|
+
header << delete_set_cookie_header(key, value)
|
385
|
+
else
|
386
|
+
header = delete_set_cookie_header(key, value)
|
387
|
+
end
|
316
388
|
|
389
|
+
return header
|
317
390
|
end
|
318
391
|
|
319
392
|
def rfc2822(time)
|
320
393
|
time.rfc2822
|
321
394
|
end
|
322
395
|
|
323
|
-
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
|
324
|
-
# of '% %b %Y'.
|
325
|
-
# It assumes that the time is in GMT to comply to the RFC 2109.
|
326
|
-
#
|
327
|
-
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
|
328
|
-
# that I'm certain someone implemented only that option.
|
329
|
-
# Do not use %a and %b from Time.strptime, it would use localized names for
|
330
|
-
# weekday and month.
|
331
|
-
#
|
332
|
-
def rfc2109(time)
|
333
|
-
wday = RFC2822_DAY_NAME[time.wday]
|
334
|
-
mon = RFC2822_MONTH_NAME[time.mon - 1]
|
335
|
-
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
|
336
|
-
end
|
337
|
-
|
338
396
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
339
397
|
# Returns nil if the header is missing or syntactically invalid.
|
340
398
|
# Returns an empty array if none of the ranges are satisfiable.
|
341
399
|
def byte_ranges(env, size)
|
342
|
-
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
|
343
400
|
get_byte_ranges env['HTTP_RANGE'], size
|
344
401
|
end
|
345
402
|
|
346
403
|
def get_byte_ranges(http_range, size)
|
347
404
|
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
405
|
+
# Ignore Range when file size is 0 to avoid a 416 error.
|
406
|
+
return nil if size.zero?
|
348
407
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
349
408
|
ranges = []
|
350
409
|
$1.split(/,\s*/).each do |range_spec|
|
351
|
-
return nil
|
352
|
-
|
353
|
-
|
354
|
-
|
410
|
+
return nil unless range_spec.include?('-')
|
411
|
+
range = range_spec.split('-')
|
412
|
+
r0, r1 = range[0], range[1]
|
413
|
+
if r0.nil? || r0.empty?
|
414
|
+
return nil if r1.nil?
|
355
415
|
# suffix-byte-range-spec, represents trailing suffix of file
|
356
416
|
r0 = size - r1.to_i
|
357
417
|
r0 = 0 if r0 < 0
|
358
418
|
r1 = size - 1
|
359
419
|
else
|
360
420
|
r0 = r0.to_i
|
361
|
-
if r1.
|
421
|
+
if r1.nil?
|
362
422
|
r1 = size - 1
|
363
423
|
else
|
364
424
|
r1 = r1.to_i
|
@@ -368,23 +428,36 @@ module Rack
|
|
368
428
|
end
|
369
429
|
ranges << (r0..r1) if r0 <= r1
|
370
430
|
end
|
431
|
+
|
432
|
+
return [] if ranges.map(&:size).sum > size
|
433
|
+
|
371
434
|
ranges
|
372
435
|
end
|
373
436
|
|
374
|
-
#
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
437
|
+
# :nocov:
|
438
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
439
|
+
# Constant time string comparison.
|
440
|
+
#
|
441
|
+
# NOTE: the values compared should be of fixed length, such as strings
|
442
|
+
# that have already been processed by HMAC. This should not be used
|
443
|
+
# on variable length plaintext strings because it could leak length info
|
444
|
+
# via timing attacks.
|
445
|
+
def secure_compare(a, b)
|
446
|
+
return false unless a.bytesize == b.bytesize
|
447
|
+
|
448
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
449
|
+
end
|
450
|
+
# :nocov:
|
451
|
+
else
|
452
|
+
def secure_compare(a, b)
|
453
|
+
return false unless a.bytesize == b.bytesize
|
382
454
|
|
383
|
-
|
455
|
+
l = a.unpack("C*")
|
384
456
|
|
385
|
-
|
386
|
-
|
387
|
-
|
457
|
+
r, i = 0, -1
|
458
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
459
|
+
r == 0
|
460
|
+
end
|
388
461
|
end
|
389
462
|
|
390
463
|
# Context allows the use of a compatible middleware at different points
|
@@ -413,101 +486,12 @@ module Rack
|
|
413
486
|
end
|
414
487
|
end
|
415
488
|
|
416
|
-
# A case-insensitive Hash that preserves the original case of a
|
417
|
-
# header when set.
|
418
|
-
#
|
419
|
-
# @api private
|
420
|
-
class HeaderHash < Hash # :nodoc:
|
421
|
-
def self.[](headers)
|
422
|
-
if headers.is_a?(HeaderHash) && !headers.frozen?
|
423
|
-
return headers
|
424
|
-
else
|
425
|
-
return self.new(headers)
|
426
|
-
end
|
427
|
-
end
|
428
|
-
|
429
|
-
def initialize(hash = {})
|
430
|
-
super()
|
431
|
-
@names = {}
|
432
|
-
hash.each { |k, v| self[k] = v }
|
433
|
-
end
|
434
|
-
|
435
|
-
# on dup/clone, we need to duplicate @names hash
|
436
|
-
def initialize_copy(other)
|
437
|
-
super
|
438
|
-
@names = other.names.dup
|
439
|
-
end
|
440
|
-
|
441
|
-
# on clear, we need to clear @names hash
|
442
|
-
def clear
|
443
|
-
super
|
444
|
-
@names.clear
|
445
|
-
end
|
446
|
-
|
447
|
-
def each
|
448
|
-
super do |k, v|
|
449
|
-
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
450
|
-
end
|
451
|
-
end
|
452
|
-
|
453
|
-
def to_hash
|
454
|
-
hash = {}
|
455
|
-
each { |k, v| hash[k] = v }
|
456
|
-
hash
|
457
|
-
end
|
458
|
-
|
459
|
-
def [](k)
|
460
|
-
super(k) || super(@names[k.downcase])
|
461
|
-
end
|
462
|
-
|
463
|
-
def []=(k, v)
|
464
|
-
canonical = k.downcase.freeze
|
465
|
-
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
|
466
|
-
@names[canonical] = k
|
467
|
-
super k, v
|
468
|
-
end
|
469
|
-
|
470
|
-
def delete(k)
|
471
|
-
canonical = k.downcase
|
472
|
-
result = super @names.delete(canonical)
|
473
|
-
result
|
474
|
-
end
|
475
|
-
|
476
|
-
def include?(k)
|
477
|
-
super || @names.include?(k.downcase)
|
478
|
-
end
|
479
|
-
|
480
|
-
alias_method :has_key?, :include?
|
481
|
-
alias_method :member?, :include?
|
482
|
-
alias_method :key?, :include?
|
483
|
-
|
484
|
-
def merge!(other)
|
485
|
-
other.each { |k, v| self[k] = v }
|
486
|
-
self
|
487
|
-
end
|
488
|
-
|
489
|
-
def merge(other)
|
490
|
-
hash = dup
|
491
|
-
hash.merge! other
|
492
|
-
end
|
493
|
-
|
494
|
-
def replace(other)
|
495
|
-
clear
|
496
|
-
other.each { |k, v| self[k] = v }
|
497
|
-
self
|
498
|
-
end
|
499
|
-
|
500
|
-
protected
|
501
|
-
def names
|
502
|
-
@names
|
503
|
-
end
|
504
|
-
end
|
505
|
-
|
506
489
|
# Every standard HTTP code mapped to the appropriate message.
|
507
490
|
# Generated with:
|
508
|
-
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv
|
509
|
-
# ruby -
|
510
|
-
#
|
491
|
+
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
|
492
|
+
# | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
|
493
|
+
# .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
|
494
|
+
# .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
|
511
495
|
HTTP_STATUS_CODES = {
|
512
496
|
100 => 'Continue',
|
513
497
|
101 => 'Switching Protocols',
|
@@ -529,7 +513,6 @@ module Rack
|
|
529
513
|
303 => 'See Other',
|
530
514
|
304 => 'Not Modified',
|
531
515
|
305 => 'Use Proxy',
|
532
|
-
306 => '(Unused)',
|
533
516
|
307 => 'Temporary Redirect',
|
534
517
|
308 => 'Permanent Redirect',
|
535
518
|
400 => 'Bad Request',
|
@@ -545,13 +528,13 @@ module Rack
|
|
545
528
|
410 => 'Gone',
|
546
529
|
411 => 'Length Required',
|
547
530
|
412 => 'Precondition Failed',
|
548
|
-
413 => '
|
531
|
+
413 => 'Content Too Large',
|
549
532
|
414 => 'URI Too Long',
|
550
533
|
415 => 'Unsupported Media Type',
|
551
534
|
416 => 'Range Not Satisfiable',
|
552
535
|
417 => 'Expectation Failed',
|
553
536
|
421 => 'Misdirected Request',
|
554
|
-
422 => 'Unprocessable
|
537
|
+
422 => 'Unprocessable Content',
|
555
538
|
423 => 'Locked',
|
556
539
|
424 => 'Failed Dependency',
|
557
540
|
425 => 'Too Early',
|
@@ -559,7 +542,7 @@ module Rack
|
|
559
542
|
428 => 'Precondition Required',
|
560
543
|
429 => 'Too Many Requests',
|
561
544
|
431 => 'Request Header Fields Too Large',
|
562
|
-
451 => 'Unavailable
|
545
|
+
451 => 'Unavailable For Legal Reasons',
|
563
546
|
500 => 'Internal Server Error',
|
564
547
|
501 => 'Not Implemented',
|
565
548
|
502 => 'Bad Gateway',
|
@@ -569,8 +552,6 @@ module Rack
|
|
569
552
|
506 => 'Variant Also Negotiates',
|
570
553
|
507 => 'Insufficient Storage',
|
571
554
|
508 => 'Loop Detected',
|
572
|
-
509 => 'Bandwidth Limit Exceeded',
|
573
|
-
510 => 'Not Extended',
|
574
555
|
511 => 'Network Authentication Required'
|
575
556
|
}
|
576
557
|
|
@@ -578,12 +559,34 @@ module Rack
|
|
578
559
|
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
579
560
|
|
580
561
|
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
581
|
-
[message.downcase.gsub(/\s
|
562
|
+
[message.downcase.gsub(/\s|-/, '_').to_sym, code]
|
582
563
|
}.flatten]
|
583
564
|
|
565
|
+
OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
|
566
|
+
payload_too_large: 413,
|
567
|
+
unprocessable_entity: 422,
|
568
|
+
bandwidth_limit_exceeded: 509,
|
569
|
+
not_extended: 510
|
570
|
+
}.freeze
|
571
|
+
private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
|
572
|
+
|
573
|
+
OBSOLETE_SYMBOL_MAPPINGS = {
|
574
|
+
payload_too_large: :content_too_large,
|
575
|
+
unprocessable_entity: :unprocessable_content
|
576
|
+
}.freeze
|
577
|
+
private_constant :OBSOLETE_SYMBOL_MAPPINGS
|
578
|
+
|
584
579
|
def status_code(status)
|
585
580
|
if status.is_a?(Symbol)
|
586
|
-
SYMBOL_TO_STATUS_CODE.fetch(status)
|
581
|
+
SYMBOL_TO_STATUS_CODE.fetch(status) do
|
582
|
+
fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
|
583
|
+
message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
|
584
|
+
if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
|
585
|
+
message = "#{message} Please use #{canonical_symbol.inspect} instead."
|
586
|
+
end
|
587
|
+
warn message, uplevel: 1
|
588
|
+
fallback_code
|
589
|
+
end
|
587
590
|
else
|
588
591
|
status.to_i
|
589
592
|
end
|