rack 2.2.3 → 3.2.7
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 +774 -69
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +384 -0
- data/SPEC.rdoc +243 -277
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +5 -1
- data/lib/rack/auth/basic.rb +1 -4
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +21 -3
- data/lib/rack/builder.rb +108 -69
- data/lib/rack/cascade.rb +2 -3
- data/lib/rack/common_logger.rb +26 -17
- data/lib/rack/conditional_get.rb +20 -16
- data/lib/rack/constants.rb +68 -0
- data/lib/rack/content_length.rb +12 -16
- data/lib/rack/content_type.rb +8 -5
- data/lib/rack/deflater.rb +40 -26
- data/lib/rack/directory.rb +15 -6
- data/lib/rack/etag.rb +17 -21
- data/lib/rack/events.rb +25 -6
- data/lib/rack/files.rb +16 -18
- data/lib/rack/head.rb +8 -8
- data/lib/rack/headers.rb +238 -0
- data/lib/rack/lint.rb +863 -705
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/media_type.rb +18 -9
- data/lib/rack/method_override.rb +6 -2
- 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 +156 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +349 -92
- data/lib/rack/multipart/uploaded_file.rb +45 -4
- data/lib/rack/multipart.rb +53 -41
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +150 -106
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +272 -141
- data/lib/rack/response.rb +151 -66
- data/lib/rack/rewindable_input.rb +27 -5
- data/lib/rack/runtime.rb +7 -6
- data/lib/rack/sendfile.rb +70 -35
- data/lib/rack/show_exceptions.rb +25 -6
- data/lib/rack/show_status.rb +17 -9
- data/lib/rack/static.rb +17 -12
- data/lib/rack/tempfile_reaper.rb +15 -4
- data/lib/rack/urlmap.rb +4 -2
- data/lib/rack/utils.rb +351 -250
- data/lib/rack/version.rb +3 -15
- data/lib/rack.rb +13 -90
- metadata +16 -45
- 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/logger.rb +0 -20
- data/lib/rack/server.rb +0 -466
- data/lib/rack/session/abstract/id.rb +0 -523
- data/lib/rack/session/cookie.rb +0 -203
- data/lib/rack/session/memcache.rb +0 -10
- data/lib/rack/session/pool.rb +0 -85
- data/rack.gemspec +0 -46
data/lib/rack/utils.rb
CHANGED
|
@@ -6,28 +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
|
|
27
|
+
URI_PARSER = defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER
|
|
24
28
|
|
|
25
29
|
class << self
|
|
26
30
|
attr_accessor :default_query_parser
|
|
27
31
|
end
|
|
28
|
-
# The default
|
|
29
|
-
# This helps prevent a rogue client from
|
|
30
|
-
|
|
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)
|
|
31
36
|
|
|
32
37
|
module_function
|
|
33
38
|
|
|
@@ -39,13 +44,13 @@ module Rack
|
|
|
39
44
|
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
|
|
40
45
|
# true URI escaping.
|
|
41
46
|
def escape_path(s)
|
|
42
|
-
|
|
47
|
+
URI_PARSER.escape s
|
|
43
48
|
end
|
|
44
49
|
|
|
45
50
|
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
|
|
46
51
|
# unescaping query parameters or form components.
|
|
47
52
|
def unescape_path(s)
|
|
48
|
-
|
|
53
|
+
URI_PARSER.unescape s
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
|
|
@@ -55,13 +60,24 @@ module Rack
|
|
|
55
60
|
end
|
|
56
61
|
|
|
57
62
|
class << self
|
|
58
|
-
attr_accessor :
|
|
63
|
+
attr_accessor :multipart_total_part_limit
|
|
64
|
+
|
|
65
|
+
attr_accessor :multipart_file_limit
|
|
66
|
+
|
|
67
|
+
# multipart_part_limit is the original name of multipart_file_limit, but
|
|
68
|
+
# the limit only counts parts with filenames.
|
|
69
|
+
alias multipart_part_limit multipart_file_limit
|
|
70
|
+
alias multipart_part_limit= multipart_file_limit=
|
|
59
71
|
end
|
|
60
72
|
|
|
61
|
-
# The maximum number of parts a request can contain. Accepting too
|
|
62
|
-
# can lead to the server running out of file handles.
|
|
73
|
+
# The maximum number of file parts a request can contain. Accepting too
|
|
74
|
+
# many parts can lead to the server running out of file handles.
|
|
63
75
|
# Set to `0` for no limit.
|
|
64
|
-
self.
|
|
76
|
+
self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
|
|
77
|
+
|
|
78
|
+
# The maximum total number of parts a request can contain. Accepting too
|
|
79
|
+
# many can lead to excessive memory use and parsing time.
|
|
80
|
+
self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
|
|
65
81
|
|
|
66
82
|
def self.param_depth_limit
|
|
67
83
|
default_query_parser.param_depth_limit
|
|
@@ -71,14 +87,6 @@ module Rack
|
|
|
71
87
|
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
|
|
72
88
|
end
|
|
73
89
|
|
|
74
|
-
def self.key_space_limit
|
|
75
|
-
default_query_parser.key_space_limit
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def self.key_space_limit=(v)
|
|
79
|
-
self.default_query_parser = self.default_query_parser.new_space_limit(v)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
90
|
if defined?(Process::CLOCK_MONOTONIC)
|
|
83
91
|
def clock_time
|
|
84
92
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -117,19 +125,19 @@ module Rack
|
|
|
117
125
|
}.join("&")
|
|
118
126
|
when Hash
|
|
119
127
|
value.map { |k, v|
|
|
120
|
-
build_nested_query(v, prefix ? "#{prefix}[#{
|
|
128
|
+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
|
121
129
|
}.delete_if(&:empty?).join('&')
|
|
122
130
|
when nil
|
|
123
|
-
prefix
|
|
131
|
+
escape(prefix)
|
|
124
132
|
else
|
|
125
133
|
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
|
126
|
-
"#{prefix}=#{escape(value)}"
|
|
134
|
+
"#{escape(prefix)}=#{escape(value)}"
|
|
127
135
|
end
|
|
128
136
|
end
|
|
129
137
|
|
|
130
138
|
def q_values(q_value_header)
|
|
131
|
-
q_value_header.to_s.split(
|
|
132
|
-
value, parameters = part.split(
|
|
139
|
+
q_value_header.to_s.split(',').map do |part|
|
|
140
|
+
value, parameters = part.split(';', 2).map(&:strip)
|
|
133
141
|
quality = 1.0
|
|
134
142
|
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
|
|
135
143
|
quality = md[1].to_f
|
|
@@ -138,6 +146,80 @@ module Rack
|
|
|
138
146
|
end
|
|
139
147
|
end
|
|
140
148
|
|
|
149
|
+
ALLOWED_FORWARDED_PARAMS = %w[by for host proto].map { |name| [name, name.to_sym] }.to_h.freeze
|
|
150
|
+
private_constant :ALLOWED_FORWARDED_PARAMS
|
|
151
|
+
|
|
152
|
+
def forwarded_values(forwarded_header)
|
|
153
|
+
return unless forwarded_header
|
|
154
|
+
header = forwarded_header.to_s.tr("\n", ";")
|
|
155
|
+
header.sub!(/\A[\s;,]+/, '')
|
|
156
|
+
num_params = num_escapes = 0
|
|
157
|
+
max_params = max_escapes = 1024
|
|
158
|
+
params = {}
|
|
159
|
+
|
|
160
|
+
# Parse parameter list
|
|
161
|
+
while i = header.index('=')
|
|
162
|
+
# Only parse up to max parameters, to avoid potential denial of service
|
|
163
|
+
num_params += 1
|
|
164
|
+
return if num_params > max_params
|
|
165
|
+
|
|
166
|
+
# Found end of parameter name, ensure forward progress in loop
|
|
167
|
+
param = header.slice!(0, i+1)
|
|
168
|
+
|
|
169
|
+
# Remove ending equals and preceding whitespace from parameter name
|
|
170
|
+
param.chomp!('=')
|
|
171
|
+
param.strip!
|
|
172
|
+
param.downcase!
|
|
173
|
+
return unless param = ALLOWED_FORWARDED_PARAMS[param]
|
|
174
|
+
|
|
175
|
+
if header[0] == '"'
|
|
176
|
+
# Parameter value is quoted, parse it, handling backslash escapes
|
|
177
|
+
header.slice!(0, 1)
|
|
178
|
+
value = String.new
|
|
179
|
+
|
|
180
|
+
while i = header.index(/(["\\])/)
|
|
181
|
+
c = $1
|
|
182
|
+
|
|
183
|
+
# Append all content until ending quote or escape
|
|
184
|
+
value << header.slice!(0, i)
|
|
185
|
+
|
|
186
|
+
# Remove either backslash or ending quote,
|
|
187
|
+
# ensures forward progress in loop
|
|
188
|
+
header.slice!(0, 1)
|
|
189
|
+
|
|
190
|
+
# stop parsing parameter value if found ending quote
|
|
191
|
+
break if c == '"'
|
|
192
|
+
|
|
193
|
+
# Only allow up to max escapes, to avoid potential denial of service
|
|
194
|
+
num_escapes += 1
|
|
195
|
+
return if num_escapes > max_escapes
|
|
196
|
+
escaped_char = header.slice!(0, 1)
|
|
197
|
+
value << escaped_char
|
|
198
|
+
end
|
|
199
|
+
else
|
|
200
|
+
if i = header.index(/[;,]/)
|
|
201
|
+
# Parameter value unquoted (which may be invalid), value ends at comma or semicolon
|
|
202
|
+
value = header.slice!(0, i)
|
|
203
|
+
value.sub!(/[\s;,]+\z/, '')
|
|
204
|
+
else
|
|
205
|
+
# If no ending semicolon, assume remainder of line is value and stop parsing
|
|
206
|
+
header.strip!
|
|
207
|
+
value = header
|
|
208
|
+
header = ''
|
|
209
|
+
end
|
|
210
|
+
value.lstrip!
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
(params[param] ||= []) << value
|
|
214
|
+
|
|
215
|
+
# skip trailing semicolons/commas/whitespace, to proceed to next parameter
|
|
216
|
+
header.sub!(/\A[\s;,]+/, '') unless header.empty?
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
params
|
|
220
|
+
end
|
|
221
|
+
module_function :forwarded_values
|
|
222
|
+
|
|
141
223
|
# Return best accept value to use, based on the algorithm
|
|
142
224
|
# in RFC 2616 Section 14. If there are multiple best
|
|
143
225
|
# matches (same specificity and quality), the value returned
|
|
@@ -152,36 +234,60 @@ module Rack
|
|
|
152
234
|
end.compact.sort_by do |match, quality|
|
|
153
235
|
(match.split('/', 2).count('*') * -10) + quality
|
|
154
236
|
end.last
|
|
155
|
-
matches
|
|
237
|
+
matches&.first
|
|
156
238
|
end
|
|
157
239
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
240
|
+
# Introduced in ERB 4.0. ERB::Escape is an alias for ERB::Utils which
|
|
241
|
+
# doesn't get monkey-patched by rails
|
|
242
|
+
if defined?(ERB::Escape) && ERB::Escape.instance_method(:html_escape)
|
|
243
|
+
define_method(:escape_html, ERB::Escape.instance_method(:html_escape))
|
|
244
|
+
# :nocov:
|
|
245
|
+
# Ruby 3.2/ERB 4.0 added ERB::Escape#html_escape, so the else
|
|
246
|
+
# branch cannot be hit on the current Ruby version.
|
|
247
|
+
else
|
|
248
|
+
require 'cgi/escape'
|
|
249
|
+
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
|
250
|
+
def escape_html(string)
|
|
251
|
+
CGI.escapeHTML(string.to_s)
|
|
252
|
+
end
|
|
253
|
+
# :nocov:
|
|
172
254
|
end
|
|
173
255
|
|
|
256
|
+
# Given an array of available encoding strings, and an array of
|
|
257
|
+
# acceptable encodings for a request, where each element of the
|
|
258
|
+
# acceptable encodings array is an array where the first element
|
|
259
|
+
# is an encoding name and the second element is the numeric
|
|
260
|
+
# priority for the encoding, return the available encoding with
|
|
261
|
+
# the highest priority.
|
|
262
|
+
#
|
|
263
|
+
# The accept_encoding argument is typically generated by calling
|
|
264
|
+
# Request#accept_encoding.
|
|
265
|
+
#
|
|
266
|
+
# Example:
|
|
267
|
+
#
|
|
268
|
+
# select_best_encoding(%w(compress gzip identity),
|
|
269
|
+
# [["compress", 0.5], ["gzip", 1.0]])
|
|
270
|
+
# # => "gzip"
|
|
271
|
+
#
|
|
272
|
+
# To reduce denial of service potential, only the first 16
|
|
273
|
+
# acceptable encodings are considered.
|
|
174
274
|
def select_best_encoding(available_encodings, accept_encoding)
|
|
175
275
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
|
176
276
|
|
|
277
|
+
# Only process the first 16 encodings
|
|
278
|
+
accept_encoding = accept_encoding[0...16]
|
|
177
279
|
expanded_accept_encoding = []
|
|
280
|
+
wildcard_seen = false
|
|
178
281
|
|
|
179
282
|
accept_encoding.each do |m, q|
|
|
180
283
|
preference = available_encodings.index(m) || available_encodings.size
|
|
181
284
|
|
|
182
285
|
if m == "*"
|
|
183
|
-
|
|
184
|
-
|
|
286
|
+
unless wildcard_seen
|
|
287
|
+
(available_encodings - accept_encoding.map(&:first)).each do |m2|
|
|
288
|
+
expanded_accept_encoding << [m2, q, preference]
|
|
289
|
+
end
|
|
290
|
+
wildcard_seen = true
|
|
185
291
|
end
|
|
186
292
|
else
|
|
187
293
|
expanded_accept_encoding << [m, q, preference]
|
|
@@ -189,7 +295,13 @@ module Rack
|
|
|
189
295
|
end
|
|
190
296
|
|
|
191
297
|
encoding_candidates = expanded_accept_encoding
|
|
192
|
-
.
|
|
298
|
+
.sort do |(_, q1, p1), (_, q2, p2)|
|
|
299
|
+
if r = (q1 <=> q2).nonzero?
|
|
300
|
+
-r
|
|
301
|
+
else
|
|
302
|
+
(p1 <=> p2).nonzero? || 0
|
|
303
|
+
end
|
|
304
|
+
end
|
|
193
305
|
.map!(&:first)
|
|
194
306
|
|
|
195
307
|
unless encoding_candidates.include?("identity")
|
|
@@ -203,24 +315,69 @@ module Rack
|
|
|
203
315
|
(encoding_candidates & available_encodings)[0]
|
|
204
316
|
end
|
|
205
317
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
318
|
+
# :call-seq:
|
|
319
|
+
# parse_cookies_header(value) -> hash
|
|
320
|
+
#
|
|
321
|
+
# Parse cookies from the provided header +value+ according to RFC6265. The
|
|
322
|
+
# syntax for cookie headers only supports semicolons. Returns a map of
|
|
323
|
+
# cookie +key+ to cookie +value+.
|
|
324
|
+
#
|
|
325
|
+
# parse_cookies_header('myname=myvalue; max-age=0')
|
|
326
|
+
# # => {"myname"=>"myvalue", "max-age"=>"0"}
|
|
327
|
+
#
|
|
328
|
+
def parse_cookies_header(value)
|
|
329
|
+
return {} unless value
|
|
209
330
|
|
|
210
|
-
|
|
211
|
-
# According to RFC 6265:
|
|
212
|
-
# The syntax for cookie headers only supports semicolons
|
|
213
|
-
# User Agent -> Server ==
|
|
214
|
-
# Cookie: SID=31d4d96e407aad42; lang=en-US
|
|
215
|
-
return {} unless header
|
|
216
|
-
header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
|
|
331
|
+
value.split(/; */n).each_with_object({}) do |cookie, cookies|
|
|
217
332
|
next if cookie.empty?
|
|
218
333
|
key, value = cookie.split('=', 2)
|
|
219
334
|
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
|
220
335
|
end
|
|
221
336
|
end
|
|
222
337
|
|
|
223
|
-
|
|
338
|
+
# :call-seq:
|
|
339
|
+
# parse_cookies(env) -> hash
|
|
340
|
+
#
|
|
341
|
+
# Parse cookies from the provided request environment using
|
|
342
|
+
# parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
|
|
343
|
+
#
|
|
344
|
+
# parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
|
|
345
|
+
# # => {'myname' => 'myvalue'}
|
|
346
|
+
#
|
|
347
|
+
def parse_cookies(env)
|
|
348
|
+
parse_cookies_header env[HTTP_COOKIE]
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# A valid cookie key according to RFC6265 and RFC2616.
|
|
352
|
+
# 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: ( ) < > @ , ; : \ " / [ ] ? = { }.
|
|
353
|
+
VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
|
|
354
|
+
private_constant :VALID_COOKIE_KEY
|
|
355
|
+
|
|
356
|
+
# :call-seq:
|
|
357
|
+
# set_cookie_header(key, value) -> encoded string
|
|
358
|
+
#
|
|
359
|
+
# Generate an encoded string using the provided +key+ and +value+ suitable
|
|
360
|
+
# for the +set-cookie+ header according to RFC6265. The +value+ may be an
|
|
361
|
+
# instance of either +String+ or +Hash+. If the cookie key is invalid (as
|
|
362
|
+
# defined by RFC6265), an +ArgumentError+ will be raised.
|
|
363
|
+
#
|
|
364
|
+
# If the cookie +value+ is an instance of +Hash+, it considers the following
|
|
365
|
+
# cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
|
|
366
|
+
# of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
|
|
367
|
+
# details about the interpretation of these fields, consult
|
|
368
|
+
# [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
|
|
369
|
+
#
|
|
370
|
+
# set_cookie_header("myname", "myvalue")
|
|
371
|
+
# # => "myname=myvalue"
|
|
372
|
+
#
|
|
373
|
+
# set_cookie_header("myname", {value: "myvalue", max_age: 10})
|
|
374
|
+
# # => "myname=myvalue; max-age=10"
|
|
375
|
+
#
|
|
376
|
+
def set_cookie_header(key, value)
|
|
377
|
+
unless key =~ VALID_COOKIE_KEY
|
|
378
|
+
raise ArgumentError, "invalid cookie key: #{key.inspect}"
|
|
379
|
+
end
|
|
380
|
+
|
|
224
381
|
case value
|
|
225
382
|
when Hash
|
|
226
383
|
domain = "; domain=#{value[:domain]}" if value[:domain]
|
|
@@ -228,134 +385,135 @@ module Rack
|
|
|
228
385
|
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
|
229
386
|
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
|
230
387
|
secure = "; secure" if value[:secure]
|
|
231
|
-
httponly = ";
|
|
388
|
+
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
|
232
389
|
same_site =
|
|
233
390
|
case value[:same_site]
|
|
234
391
|
when false, nil
|
|
235
392
|
nil
|
|
236
393
|
when :none, 'None', :None
|
|
237
|
-
';
|
|
394
|
+
'; samesite=none'
|
|
238
395
|
when :lax, 'Lax', :Lax
|
|
239
|
-
';
|
|
396
|
+
'; samesite=lax'
|
|
240
397
|
when true, :strict, 'Strict', :Strict
|
|
241
|
-
';
|
|
398
|
+
'; samesite=strict'
|
|
242
399
|
else
|
|
243
|
-
raise ArgumentError, "Invalid
|
|
400
|
+
raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
|
|
244
401
|
end
|
|
402
|
+
partitioned = "; partitioned" if value[:partitioned]
|
|
245
403
|
value = value[:value]
|
|
246
404
|
end
|
|
405
|
+
|
|
247
406
|
value = [value] unless Array === value
|
|
248
407
|
|
|
249
|
-
|
|
250
|
-
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
|
|
408
|
+
return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
|
|
409
|
+
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
|
|
410
|
+
end
|
|
251
411
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
412
|
+
# :call-seq:
|
|
413
|
+
# set_cookie_header!(headers, key, value) -> header value
|
|
414
|
+
#
|
|
415
|
+
# Append a cookie in the specified headers with the given cookie +key+ and
|
|
416
|
+
# +value+ using set_cookie_header.
|
|
417
|
+
#
|
|
418
|
+
# If the headers already contains a +set-cookie+ key, it will be converted
|
|
419
|
+
# to an +Array+ if not already, and appended to.
|
|
420
|
+
def set_cookie_header!(headers, key, value)
|
|
421
|
+
if header = headers[SET_COOKIE]
|
|
422
|
+
if header.is_a?(Array)
|
|
423
|
+
header << set_cookie_header(key, value)
|
|
424
|
+
else
|
|
425
|
+
headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
|
|
426
|
+
end
|
|
259
427
|
else
|
|
260
|
-
|
|
428
|
+
headers[SET_COOKIE] = set_cookie_header(key, value)
|
|
261
429
|
end
|
|
262
430
|
end
|
|
263
431
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
432
|
+
# :call-seq:
|
|
433
|
+
# delete_set_cookie_header(key, value = {}) -> encoded string
|
|
434
|
+
#
|
|
435
|
+
# Generate an encoded string based on the given +key+ and +value+ using
|
|
436
|
+
# set_cookie_header for the purpose of causing the specified cookie to be
|
|
437
|
+
# deleted. The +value+ may be an instance of +Hash+ and can include
|
|
438
|
+
# attributes as outlined by set_cookie_header. The encoded cookie will have
|
|
439
|
+
# a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
|
|
440
|
+
# +value+. When used with the +set-cookie+ header, it will cause the client
|
|
441
|
+
# to *remove* any matching cookie.
|
|
442
|
+
#
|
|
443
|
+
# delete_set_cookie_header("myname")
|
|
444
|
+
# # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
445
|
+
#
|
|
446
|
+
def delete_set_cookie_header(key, value = {})
|
|
447
|
+
set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
|
|
267
448
|
end
|
|
268
449
|
|
|
269
|
-
def
|
|
270
|
-
|
|
271
|
-
when nil, ''
|
|
272
|
-
cookies = []
|
|
273
|
-
when String
|
|
274
|
-
cookies = header.split("\n")
|
|
275
|
-
when Array
|
|
276
|
-
cookies = header
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
key = escape(key)
|
|
280
|
-
domain = value[:domain]
|
|
281
|
-
path = value[:path]
|
|
282
|
-
regexp = if domain
|
|
283
|
-
if path
|
|
284
|
-
/\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
|
|
285
|
-
else
|
|
286
|
-
/\A#{key}=.*domain=#{domain}(?:;|$)/
|
|
287
|
-
end
|
|
288
|
-
elsif path
|
|
289
|
-
/\A#{key}=.*path=#{path}(?:;|$)/
|
|
290
|
-
else
|
|
291
|
-
/\A#{key}=/
|
|
292
|
-
end
|
|
450
|
+
def delete_cookie_header!(headers, key, value = {})
|
|
451
|
+
headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
|
|
293
452
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
cookies.join("\n")
|
|
453
|
+
return nil
|
|
297
454
|
end
|
|
298
455
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
456
|
+
# :call-seq:
|
|
457
|
+
# delete_set_cookie_header!(header, key, value = {}) -> header value
|
|
458
|
+
#
|
|
459
|
+
# Set an expired cookie in the specified headers with the given cookie
|
|
460
|
+
# +key+ and +value+ using delete_set_cookie_header. This causes
|
|
461
|
+
# the client to immediately delete the specified cookie.
|
|
462
|
+
#
|
|
463
|
+
# delete_set_cookie_header!(nil, "mycookie")
|
|
464
|
+
# # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
465
|
+
#
|
|
466
|
+
# If the header is non-nil, it will be modified in place.
|
|
467
|
+
#
|
|
468
|
+
# header = []
|
|
469
|
+
# delete_set_cookie_header!(header, "mycookie")
|
|
470
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
|
471
|
+
# header
|
|
472
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
|
473
|
+
#
|
|
474
|
+
def delete_set_cookie_header!(header, key, value = {})
|
|
475
|
+
if header
|
|
476
|
+
header = Array(header)
|
|
477
|
+
header << delete_set_cookie_header(key, value)
|
|
478
|
+
else
|
|
479
|
+
header = delete_set_cookie_header(key, value)
|
|
480
|
+
end
|
|
313
481
|
|
|
482
|
+
return header
|
|
314
483
|
end
|
|
315
484
|
|
|
316
485
|
def rfc2822(time)
|
|
317
486
|
time.rfc2822
|
|
318
487
|
end
|
|
319
488
|
|
|
320
|
-
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
|
|
321
|
-
# of '% %b %Y'.
|
|
322
|
-
# It assumes that the time is in GMT to comply to the RFC 2109.
|
|
323
|
-
#
|
|
324
|
-
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
|
|
325
|
-
# that I'm certain someone implemented only that option.
|
|
326
|
-
# Do not use %a and %b from Time.strptime, it would use localized names for
|
|
327
|
-
# weekday and month.
|
|
328
|
-
#
|
|
329
|
-
def rfc2109(time)
|
|
330
|
-
wday = Time::RFC2822_DAY_NAME[time.wday]
|
|
331
|
-
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
|
|
332
|
-
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
|
|
333
|
-
end
|
|
334
|
-
|
|
335
489
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
|
336
490
|
# Returns nil if the header is missing or syntactically invalid.
|
|
337
491
|
# Returns an empty array if none of the ranges are satisfiable.
|
|
338
|
-
def byte_ranges(env, size)
|
|
339
|
-
|
|
340
|
-
get_byte_ranges env['HTTP_RANGE'], size
|
|
492
|
+
def byte_ranges(env, size, max_ranges: 100)
|
|
493
|
+
get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
|
|
341
494
|
end
|
|
342
495
|
|
|
343
|
-
def get_byte_ranges(http_range, size)
|
|
496
|
+
def get_byte_ranges(http_range, size, max_ranges: 100)
|
|
344
497
|
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
|
498
|
+
# Ignore Range when file size is 0 to avoid a 416 error.
|
|
499
|
+
return nil if size.zero?
|
|
345
500
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
|
501
|
+
byte_range = $1
|
|
502
|
+
return nil if byte_range.count(',') >= max_ranges
|
|
346
503
|
ranges = []
|
|
347
|
-
|
|
348
|
-
return nil
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
504
|
+
byte_range.split(/,[ \t]*/).each do |range_spec|
|
|
505
|
+
return nil unless range_spec.include?('-')
|
|
506
|
+
range = range_spec.split('-')
|
|
507
|
+
r0, r1 = range[0], range[1]
|
|
508
|
+
if r0.nil? || r0.empty?
|
|
509
|
+
return nil if r1.nil?
|
|
352
510
|
# suffix-byte-range-spec, represents trailing suffix of file
|
|
353
511
|
r0 = size - r1.to_i
|
|
354
512
|
r0 = 0 if r0 < 0
|
|
355
513
|
r1 = size - 1
|
|
356
514
|
else
|
|
357
515
|
r0 = r0.to_i
|
|
358
|
-
if r1.
|
|
516
|
+
if r1.nil?
|
|
359
517
|
r1 = size - 1
|
|
360
518
|
else
|
|
361
519
|
r1 = r1.to_i
|
|
@@ -365,23 +523,36 @@ module Rack
|
|
|
365
523
|
end
|
|
366
524
|
ranges << (r0..r1) if r0 <= r1
|
|
367
525
|
end
|
|
526
|
+
|
|
527
|
+
return [] if ranges.map(&:size).sum > size
|
|
528
|
+
|
|
368
529
|
ranges
|
|
369
530
|
end
|
|
370
531
|
|
|
371
|
-
#
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
532
|
+
# :nocov:
|
|
533
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
|
534
|
+
# Constant time string comparison.
|
|
535
|
+
#
|
|
536
|
+
# NOTE: the values compared should be of fixed length, such as strings
|
|
537
|
+
# that have already been processed by HMAC. This should not be used
|
|
538
|
+
# on variable length plaintext strings because it could leak length info
|
|
539
|
+
# via timing attacks.
|
|
540
|
+
def secure_compare(a, b)
|
|
541
|
+
return false unless a.bytesize == b.bytesize
|
|
379
542
|
|
|
380
|
-
|
|
543
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
544
|
+
end
|
|
545
|
+
# :nocov:
|
|
546
|
+
else
|
|
547
|
+
def secure_compare(a, b)
|
|
548
|
+
return false unless a.bytesize == b.bytesize
|
|
549
|
+
|
|
550
|
+
l = a.unpack("C*")
|
|
381
551
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
552
|
+
r, i = 0, -1
|
|
553
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
|
554
|
+
r == 0
|
|
555
|
+
end
|
|
385
556
|
end
|
|
386
557
|
|
|
387
558
|
# Context allows the use of a compatible middleware at different points
|
|
@@ -410,101 +581,12 @@ module Rack
|
|
|
410
581
|
end
|
|
411
582
|
end
|
|
412
583
|
|
|
413
|
-
# A case-insensitive Hash that preserves the original case of a
|
|
414
|
-
# header when set.
|
|
415
|
-
#
|
|
416
|
-
# @api private
|
|
417
|
-
class HeaderHash < Hash # :nodoc:
|
|
418
|
-
def self.[](headers)
|
|
419
|
-
if headers.is_a?(HeaderHash) && !headers.frozen?
|
|
420
|
-
return headers
|
|
421
|
-
else
|
|
422
|
-
return self.new(headers)
|
|
423
|
-
end
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
def initialize(hash = {})
|
|
427
|
-
super()
|
|
428
|
-
@names = {}
|
|
429
|
-
hash.each { |k, v| self[k] = v }
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
# on dup/clone, we need to duplicate @names hash
|
|
433
|
-
def initialize_copy(other)
|
|
434
|
-
super
|
|
435
|
-
@names = other.names.dup
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
# on clear, we need to clear @names hash
|
|
439
|
-
def clear
|
|
440
|
-
super
|
|
441
|
-
@names.clear
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
def each
|
|
445
|
-
super do |k, v|
|
|
446
|
-
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
def to_hash
|
|
451
|
-
hash = {}
|
|
452
|
-
each { |k, v| hash[k] = v }
|
|
453
|
-
hash
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def [](k)
|
|
457
|
-
super(k) || super(@names[k.downcase])
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
def []=(k, v)
|
|
461
|
-
canonical = k.downcase.freeze
|
|
462
|
-
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
|
|
463
|
-
@names[canonical] = k
|
|
464
|
-
super k, v
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
def delete(k)
|
|
468
|
-
canonical = k.downcase
|
|
469
|
-
result = super @names.delete(canonical)
|
|
470
|
-
result
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
def include?(k)
|
|
474
|
-
super || @names.include?(k.downcase)
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
alias_method :has_key?, :include?
|
|
478
|
-
alias_method :member?, :include?
|
|
479
|
-
alias_method :key?, :include?
|
|
480
|
-
|
|
481
|
-
def merge!(other)
|
|
482
|
-
other.each { |k, v| self[k] = v }
|
|
483
|
-
self
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
def merge(other)
|
|
487
|
-
hash = dup
|
|
488
|
-
hash.merge! other
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
def replace(other)
|
|
492
|
-
clear
|
|
493
|
-
other.each { |k, v| self[k] = v }
|
|
494
|
-
self
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
protected
|
|
498
|
-
def names
|
|
499
|
-
@names
|
|
500
|
-
end
|
|
501
|
-
end
|
|
502
|
-
|
|
503
584
|
# Every standard HTTP code mapped to the appropriate message.
|
|
504
585
|
# Generated with:
|
|
505
|
-
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv
|
|
506
|
-
# ruby -
|
|
507
|
-
#
|
|
586
|
+
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
|
|
587
|
+
# | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
|
|
588
|
+
# .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
|
|
589
|
+
# .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
|
|
508
590
|
HTTP_STATUS_CODES = {
|
|
509
591
|
100 => 'Continue',
|
|
510
592
|
101 => 'Switching Protocols',
|
|
@@ -526,7 +608,6 @@ module Rack
|
|
|
526
608
|
303 => 'See Other',
|
|
527
609
|
304 => 'Not Modified',
|
|
528
610
|
305 => 'Use Proxy',
|
|
529
|
-
306 => '(Unused)',
|
|
530
611
|
307 => 'Temporary Redirect',
|
|
531
612
|
308 => 'Permanent Redirect',
|
|
532
613
|
400 => 'Bad Request',
|
|
@@ -542,13 +623,13 @@ module Rack
|
|
|
542
623
|
410 => 'Gone',
|
|
543
624
|
411 => 'Length Required',
|
|
544
625
|
412 => 'Precondition Failed',
|
|
545
|
-
413 => '
|
|
626
|
+
413 => 'Content Too Large',
|
|
546
627
|
414 => 'URI Too Long',
|
|
547
628
|
415 => 'Unsupported Media Type',
|
|
548
629
|
416 => 'Range Not Satisfiable',
|
|
549
630
|
417 => 'Expectation Failed',
|
|
550
631
|
421 => 'Misdirected Request',
|
|
551
|
-
422 => 'Unprocessable
|
|
632
|
+
422 => 'Unprocessable Content',
|
|
552
633
|
423 => 'Locked',
|
|
553
634
|
424 => 'Failed Dependency',
|
|
554
635
|
425 => 'Too Early',
|
|
@@ -556,7 +637,7 @@ module Rack
|
|
|
556
637
|
428 => 'Precondition Required',
|
|
557
638
|
429 => 'Too Many Requests',
|
|
558
639
|
431 => 'Request Header Fields Too Large',
|
|
559
|
-
451 => 'Unavailable
|
|
640
|
+
451 => 'Unavailable For Legal Reasons',
|
|
560
641
|
500 => 'Internal Server Error',
|
|
561
642
|
501 => 'Not Implemented',
|
|
562
643
|
502 => 'Bad Gateway',
|
|
@@ -566,8 +647,6 @@ module Rack
|
|
|
566
647
|
506 => 'Variant Also Negotiates',
|
|
567
648
|
507 => 'Insufficient Storage',
|
|
568
649
|
508 => 'Loop Detected',
|
|
569
|
-
509 => 'Bandwidth Limit Exceeded',
|
|
570
|
-
510 => 'Not Extended',
|
|
571
650
|
511 => 'Network Authentication Required'
|
|
572
651
|
}
|
|
573
652
|
|
|
@@ -575,12 +654,34 @@ module Rack
|
|
|
575
654
|
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
|
576
655
|
|
|
577
656
|
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
|
578
|
-
[message.downcase.gsub(/\s
|
|
657
|
+
[message.downcase.gsub(/\s|-/, '_').to_sym, code]
|
|
579
658
|
}.flatten]
|
|
580
659
|
|
|
660
|
+
OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
|
|
661
|
+
payload_too_large: 413,
|
|
662
|
+
unprocessable_entity: 422,
|
|
663
|
+
bandwidth_limit_exceeded: 509,
|
|
664
|
+
not_extended: 510
|
|
665
|
+
}.freeze
|
|
666
|
+
private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
|
|
667
|
+
|
|
668
|
+
OBSOLETE_SYMBOL_MAPPINGS = {
|
|
669
|
+
payload_too_large: :content_too_large,
|
|
670
|
+
unprocessable_entity: :unprocessable_content
|
|
671
|
+
}.freeze
|
|
672
|
+
private_constant :OBSOLETE_SYMBOL_MAPPINGS
|
|
673
|
+
|
|
581
674
|
def status_code(status)
|
|
582
675
|
if status.is_a?(Symbol)
|
|
583
|
-
SYMBOL_TO_STATUS_CODE.fetch(status)
|
|
676
|
+
SYMBOL_TO_STATUS_CODE.fetch(status) do
|
|
677
|
+
fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
|
|
678
|
+
message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
|
|
679
|
+
if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
|
|
680
|
+
message = "#{message} Please use #{canonical_symbol.inspect} instead."
|
|
681
|
+
end
|
|
682
|
+
warn message, uplevel: 3
|
|
683
|
+
fallback_code
|
|
684
|
+
end
|
|
584
685
|
else
|
|
585
686
|
status.to_i
|
|
586
687
|
end
|