rack 2.2.23 → 3.2.5
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 +554 -83
- 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 -3
- 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 +22 -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 +10 -4
- data/lib/rack/etag.rb +17 -23
- 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 +817 -648
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/media_type.rb +6 -7
- data/lib/rack/method_override.rb +5 -1
- data/lib/rack/mime.rb +14 -5
- data/lib/rack/mock.rb +1 -300
- 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 +240 -123
- data/lib/rack/multipart/uploaded_file.rb +45 -4
- data/lib/rack/multipart.rb +53 -40
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +116 -121
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +272 -144
- 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 +37 -32
- data/lib/rack/show_exceptions.rb +25 -6
- data/lib/rack/show_status.rb +17 -9
- data/lib/rack/static.rb +11 -15
- data/lib/rack/tempfile_reaper.rb +15 -4
- data/lib/rack/urlmap.rb +3 -1
- data/lib/rack/utils.rb +234 -275
- data/lib/rack/version.rb +3 -15
- data/lib/rack.rb +13 -90
- metadata +15 -41
- data/README.rdoc +0 -355
- 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 -34
- 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 -90
- 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,60 +174,36 @@ 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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
# :nocov:
|
|
185
|
+
# Ruby 3.2/ERB 4.0 added ERB::Escape#html_escape, so the else
|
|
186
|
+
# branch cannot be hit on the current Ruby version.
|
|
187
|
+
else
|
|
188
|
+
require 'cgi/escape'
|
|
189
|
+
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
|
190
|
+
def escape_html(string)
|
|
191
|
+
CGI.escapeHTML(string.to_s)
|
|
192
|
+
end
|
|
193
|
+
# :nocov:
|
|
187
194
|
end
|
|
188
195
|
|
|
189
|
-
# Given an array of available encoding strings, and an array of
|
|
190
|
-
# acceptable encodings for a request, where each element of the
|
|
191
|
-
# acceptable encodings array is an array where the first element
|
|
192
|
-
# is an encoding name and the second element is the numeric
|
|
193
|
-
# priority for the encoding, return the available encoding with
|
|
194
|
-
# the highest priority.
|
|
195
|
-
#
|
|
196
|
-
# The accept_encoding argument is typically generated by calling
|
|
197
|
-
# Request#accept_encoding.
|
|
198
|
-
#
|
|
199
|
-
# Example:
|
|
200
|
-
#
|
|
201
|
-
# select_best_encoding(%w(compress gzip identity),
|
|
202
|
-
# [["compress", 0.5], ["gzip", 1.0]])
|
|
203
|
-
# # => "gzip"
|
|
204
|
-
#
|
|
205
|
-
# To reduce denial of service potential, only the first 16
|
|
206
|
-
# acceptable encodings are considered.
|
|
207
196
|
def select_best_encoding(available_encodings, accept_encoding)
|
|
208
197
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
|
209
198
|
|
|
210
|
-
# Only process the first 16 encodings
|
|
211
|
-
accept_encoding = accept_encoding[0...16]
|
|
212
199
|
expanded_accept_encoding = []
|
|
213
|
-
wildcard_seen = false
|
|
214
200
|
|
|
215
201
|
accept_encoding.each do |m, q|
|
|
216
202
|
preference = available_encodings.index(m) || available_encodings.size
|
|
217
203
|
|
|
218
204
|
if m == "*"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
expanded_accept_encoding << [m2, q, preference]
|
|
222
|
-
end
|
|
223
|
-
wildcard_seen = true
|
|
205
|
+
(available_encodings - accept_encoding.map(&:first)).each do |m2|
|
|
206
|
+
expanded_accept_encoding << [m2, q, preference]
|
|
224
207
|
end
|
|
225
208
|
else
|
|
226
209
|
expanded_accept_encoding << [m, q, preference]
|
|
@@ -228,13 +211,7 @@ module Rack
|
|
|
228
211
|
end
|
|
229
212
|
|
|
230
213
|
encoding_candidates = expanded_accept_encoding
|
|
231
|
-
.
|
|
232
|
-
if r = (q1 <=> q2).nonzero?
|
|
233
|
-
-r
|
|
234
|
-
else
|
|
235
|
-
(p1 <=> p2).nonzero? || 0
|
|
236
|
-
end
|
|
237
|
-
end
|
|
214
|
+
.sort_by { |_, q, p| [-q, p] }
|
|
238
215
|
.map!(&:first)
|
|
239
216
|
|
|
240
217
|
unless encoding_candidates.include?("identity")
|
|
@@ -248,24 +225,69 @@ module Rack
|
|
|
248
225
|
(encoding_candidates & available_encodings)[0]
|
|
249
226
|
end
|
|
250
227
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
228
|
+
# :call-seq:
|
|
229
|
+
# parse_cookies_header(value) -> hash
|
|
230
|
+
#
|
|
231
|
+
# Parse cookies from the provided header +value+ according to RFC6265. The
|
|
232
|
+
# syntax for cookie headers only supports semicolons. Returns a map of
|
|
233
|
+
# cookie +key+ to cookie +value+.
|
|
234
|
+
#
|
|
235
|
+
# parse_cookies_header('myname=myvalue; max-age=0')
|
|
236
|
+
# # => {"myname"=>"myvalue", "max-age"=>"0"}
|
|
237
|
+
#
|
|
238
|
+
def parse_cookies_header(value)
|
|
239
|
+
return {} unless value
|
|
254
240
|
|
|
255
|
-
|
|
256
|
-
# According to RFC 6265:
|
|
257
|
-
# The syntax for cookie headers only supports semicolons
|
|
258
|
-
# User Agent -> Server ==
|
|
259
|
-
# Cookie: SID=31d4d96e407aad42; lang=en-US
|
|
260
|
-
return {} unless header
|
|
261
|
-
header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
|
|
241
|
+
value.split(/; */n).each_with_object({}) do |cookie, cookies|
|
|
262
242
|
next if cookie.empty?
|
|
263
243
|
key, value = cookie.split('=', 2)
|
|
264
244
|
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
|
265
245
|
end
|
|
266
246
|
end
|
|
267
247
|
|
|
268
|
-
|
|
248
|
+
# :call-seq:
|
|
249
|
+
# parse_cookies(env) -> hash
|
|
250
|
+
#
|
|
251
|
+
# Parse cookies from the provided request environment using
|
|
252
|
+
# parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
|
|
253
|
+
#
|
|
254
|
+
# parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
|
|
255
|
+
# # => {'myname' => 'myvalue'}
|
|
256
|
+
#
|
|
257
|
+
def parse_cookies(env)
|
|
258
|
+
parse_cookies_header env[HTTP_COOKIE]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# A valid cookie key according to RFC6265 and RFC2616.
|
|
262
|
+
# 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: ( ) < > @ , ; : \ " / [ ] ? = { }.
|
|
263
|
+
VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
|
|
264
|
+
private_constant :VALID_COOKIE_KEY
|
|
265
|
+
|
|
266
|
+
# :call-seq:
|
|
267
|
+
# set_cookie_header(key, value) -> encoded string
|
|
268
|
+
#
|
|
269
|
+
# Generate an encoded string using the provided +key+ and +value+ suitable
|
|
270
|
+
# for the +set-cookie+ header according to RFC6265. The +value+ may be an
|
|
271
|
+
# instance of either +String+ or +Hash+. If the cookie key is invalid (as
|
|
272
|
+
# defined by RFC6265), an +ArgumentError+ will be raised.
|
|
273
|
+
#
|
|
274
|
+
# If the cookie +value+ is an instance of +Hash+, it considers the following
|
|
275
|
+
# cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
|
|
276
|
+
# of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
|
|
277
|
+
# details about the interpretation of these fields, consult
|
|
278
|
+
# [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
|
|
279
|
+
#
|
|
280
|
+
# set_cookie_header("myname", "myvalue")
|
|
281
|
+
# # => "myname=myvalue"
|
|
282
|
+
#
|
|
283
|
+
# set_cookie_header("myname", {value: "myvalue", max_age: 10})
|
|
284
|
+
# # => "myname=myvalue; max-age=10"
|
|
285
|
+
#
|
|
286
|
+
def set_cookie_header(key, value)
|
|
287
|
+
unless key =~ VALID_COOKIE_KEY
|
|
288
|
+
raise ArgumentError, "invalid cookie key: #{key.inspect}"
|
|
289
|
+
end
|
|
290
|
+
|
|
269
291
|
case value
|
|
270
292
|
when Hash
|
|
271
293
|
domain = "; domain=#{value[:domain]}" if value[:domain]
|
|
@@ -273,124 +295,121 @@ module Rack
|
|
|
273
295
|
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
|
274
296
|
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
|
275
297
|
secure = "; secure" if value[:secure]
|
|
276
|
-
httponly = ";
|
|
298
|
+
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
|
277
299
|
same_site =
|
|
278
300
|
case value[:same_site]
|
|
279
301
|
when false, nil
|
|
280
302
|
nil
|
|
281
303
|
when :none, 'None', :None
|
|
282
|
-
';
|
|
304
|
+
'; samesite=none'
|
|
283
305
|
when :lax, 'Lax', :Lax
|
|
284
|
-
';
|
|
306
|
+
'; samesite=lax'
|
|
285
307
|
when true, :strict, 'Strict', :Strict
|
|
286
|
-
';
|
|
308
|
+
'; samesite=strict'
|
|
287
309
|
else
|
|
288
|
-
raise ArgumentError, "Invalid
|
|
310
|
+
raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
|
|
289
311
|
end
|
|
312
|
+
partitioned = "; partitioned" if value[:partitioned]
|
|
290
313
|
value = value[:value]
|
|
291
314
|
end
|
|
315
|
+
|
|
292
316
|
value = [value] unless Array === value
|
|
293
317
|
|
|
294
|
-
|
|
295
|
-
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
|
|
318
|
+
return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
|
|
319
|
+
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
|
|
320
|
+
end
|
|
296
321
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
322
|
+
# :call-seq:
|
|
323
|
+
# set_cookie_header!(headers, key, value) -> header value
|
|
324
|
+
#
|
|
325
|
+
# Append a cookie in the specified headers with the given cookie +key+ and
|
|
326
|
+
# +value+ using set_cookie_header.
|
|
327
|
+
#
|
|
328
|
+
# If the headers already contains a +set-cookie+ key, it will be converted
|
|
329
|
+
# to an +Array+ if not already, and appended to.
|
|
330
|
+
def set_cookie_header!(headers, key, value)
|
|
331
|
+
if header = headers[SET_COOKIE]
|
|
332
|
+
if header.is_a?(Array)
|
|
333
|
+
header << set_cookie_header(key, value)
|
|
334
|
+
else
|
|
335
|
+
headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
|
|
336
|
+
end
|
|
304
337
|
else
|
|
305
|
-
|
|
338
|
+
headers[SET_COOKIE] = set_cookie_header(key, value)
|
|
306
339
|
end
|
|
307
340
|
end
|
|
308
341
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
342
|
+
# :call-seq:
|
|
343
|
+
# delete_set_cookie_header(key, value = {}) -> encoded string
|
|
344
|
+
#
|
|
345
|
+
# Generate an encoded string based on the given +key+ and +value+ using
|
|
346
|
+
# set_cookie_header for the purpose of causing the specified cookie to be
|
|
347
|
+
# deleted. The +value+ may be an instance of +Hash+ and can include
|
|
348
|
+
# attributes as outlined by set_cookie_header. The encoded cookie will have
|
|
349
|
+
# a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
|
|
350
|
+
# +value+. When used with the +set-cookie+ header, it will cause the client
|
|
351
|
+
# to *remove* any matching cookie.
|
|
352
|
+
#
|
|
353
|
+
# delete_set_cookie_header("myname")
|
|
354
|
+
# # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
355
|
+
#
|
|
356
|
+
def delete_set_cookie_header(key, value = {})
|
|
357
|
+
set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
|
|
312
358
|
end
|
|
313
359
|
|
|
314
|
-
def
|
|
315
|
-
|
|
316
|
-
when nil, ''
|
|
317
|
-
cookies = []
|
|
318
|
-
when String
|
|
319
|
-
cookies = header.split("\n")
|
|
320
|
-
when Array
|
|
321
|
-
cookies = header
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
key = escape(key)
|
|
325
|
-
domain = value[:domain]
|
|
326
|
-
path = value[:path]
|
|
327
|
-
regexp = if domain
|
|
328
|
-
if path
|
|
329
|
-
/\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
|
|
330
|
-
else
|
|
331
|
-
/\A#{key}=.*domain=#{domain}(?:;|$)/
|
|
332
|
-
end
|
|
333
|
-
elsif path
|
|
334
|
-
/\A#{key}=.*path=#{path}(?:;|$)/
|
|
335
|
-
else
|
|
336
|
-
/\A#{key}=/
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
cookies.reject! { |cookie| regexp.match? cookie }
|
|
360
|
+
def delete_cookie_header!(headers, key, value = {})
|
|
361
|
+
headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
|
|
340
362
|
|
|
341
|
-
|
|
363
|
+
return nil
|
|
342
364
|
end
|
|
343
365
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
#
|
|
350
|
-
#
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
366
|
+
# :call-seq:
|
|
367
|
+
# delete_set_cookie_header!(header, key, value = {}) -> header value
|
|
368
|
+
#
|
|
369
|
+
# Set an expired cookie in the specified headers with the given cookie
|
|
370
|
+
# +key+ and +value+ using delete_set_cookie_header. This causes
|
|
371
|
+
# the client to immediately delete the specified cookie.
|
|
372
|
+
#
|
|
373
|
+
# delete_set_cookie_header!(nil, "mycookie")
|
|
374
|
+
# # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
375
|
+
#
|
|
376
|
+
# If the header is non-nil, it will be modified in place.
|
|
377
|
+
#
|
|
378
|
+
# header = []
|
|
379
|
+
# delete_set_cookie_header!(header, "mycookie")
|
|
380
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
|
381
|
+
# header
|
|
382
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
|
383
|
+
#
|
|
384
|
+
def delete_set_cookie_header!(header, key, value = {})
|
|
385
|
+
if header
|
|
386
|
+
header = Array(header)
|
|
387
|
+
header << delete_set_cookie_header(key, value)
|
|
388
|
+
else
|
|
389
|
+
header = delete_set_cookie_header(key, value)
|
|
390
|
+
end
|
|
358
391
|
|
|
392
|
+
return header
|
|
359
393
|
end
|
|
360
394
|
|
|
361
395
|
def rfc2822(time)
|
|
362
396
|
time.rfc2822
|
|
363
397
|
end
|
|
364
398
|
|
|
365
|
-
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
|
|
366
|
-
# of '% %b %Y'.
|
|
367
|
-
# It assumes that the time is in GMT to comply to the RFC 2109.
|
|
368
|
-
#
|
|
369
|
-
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
|
|
370
|
-
# that I'm certain someone implemented only that option.
|
|
371
|
-
# Do not use %a and %b from Time.strptime, it would use localized names for
|
|
372
|
-
# weekday and month.
|
|
373
|
-
#
|
|
374
|
-
def rfc2109(time)
|
|
375
|
-
wday = RFC2822_DAY_NAME[time.wday]
|
|
376
|
-
mon = RFC2822_MONTH_NAME[time.mon - 1]
|
|
377
|
-
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
|
|
378
|
-
end
|
|
379
|
-
|
|
380
399
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
|
381
400
|
# Returns nil if the header is missing or syntactically invalid.
|
|
382
401
|
# Returns an empty array if none of the ranges are satisfiable.
|
|
383
|
-
def byte_ranges(env, size
|
|
384
|
-
get_byte_ranges env['HTTP_RANGE'], size
|
|
402
|
+
def byte_ranges(env, size)
|
|
403
|
+
get_byte_ranges env['HTTP_RANGE'], size
|
|
385
404
|
end
|
|
386
405
|
|
|
387
|
-
def get_byte_ranges(http_range, size
|
|
406
|
+
def get_byte_ranges(http_range, size)
|
|
388
407
|
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
|
408
|
+
# Ignore Range when file size is 0 to avoid a 416 error.
|
|
409
|
+
return nil if size.zero?
|
|
389
410
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
|
390
|
-
byte_range = $1
|
|
391
|
-
return nil if byte_range.count(',') >= max_ranges
|
|
392
411
|
ranges = []
|
|
393
|
-
|
|
412
|
+
$1.split(/,[ \t]*/).each do |range_spec|
|
|
394
413
|
return nil unless range_spec.include?('-')
|
|
395
414
|
range = range_spec.split('-')
|
|
396
415
|
r0, r1 = range[0], range[1]
|
|
@@ -413,25 +432,35 @@ module Rack
|
|
|
413
432
|
ranges << (r0..r1) if r0 <= r1
|
|
414
433
|
end
|
|
415
434
|
|
|
416
|
-
return [] if ranges.map(&:size).
|
|
435
|
+
return [] if ranges.map(&:size).sum > size
|
|
417
436
|
|
|
418
437
|
ranges
|
|
419
438
|
end
|
|
420
439
|
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
440
|
+
# :nocov:
|
|
441
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
|
442
|
+
# Constant time string comparison.
|
|
443
|
+
#
|
|
444
|
+
# NOTE: the values compared should be of fixed length, such as strings
|
|
445
|
+
# that have already been processed by HMAC. This should not be used
|
|
446
|
+
# on variable length plaintext strings because it could leak length info
|
|
447
|
+
# via timing attacks.
|
|
448
|
+
def secure_compare(a, b)
|
|
449
|
+
return false unless a.bytesize == b.bytesize
|
|
429
450
|
|
|
430
|
-
|
|
451
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
452
|
+
end
|
|
453
|
+
# :nocov:
|
|
454
|
+
else
|
|
455
|
+
def secure_compare(a, b)
|
|
456
|
+
return false unless a.bytesize == b.bytesize
|
|
431
457
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
458
|
+
l = a.unpack("C*")
|
|
459
|
+
|
|
460
|
+
r, i = 0, -1
|
|
461
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
|
462
|
+
r == 0
|
|
463
|
+
end
|
|
435
464
|
end
|
|
436
465
|
|
|
437
466
|
# Context allows the use of a compatible middleware at different points
|
|
@@ -460,101 +489,12 @@ module Rack
|
|
|
460
489
|
end
|
|
461
490
|
end
|
|
462
491
|
|
|
463
|
-
# A case-insensitive Hash that preserves the original case of a
|
|
464
|
-
# header when set.
|
|
465
|
-
#
|
|
466
|
-
# @api private
|
|
467
|
-
class HeaderHash < Hash # :nodoc:
|
|
468
|
-
def self.[](headers)
|
|
469
|
-
if headers.is_a?(HeaderHash) && !headers.frozen?
|
|
470
|
-
return headers
|
|
471
|
-
else
|
|
472
|
-
return self.new(headers)
|
|
473
|
-
end
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
def initialize(hash = {})
|
|
477
|
-
super()
|
|
478
|
-
@names = {}
|
|
479
|
-
hash.each { |k, v| self[k] = v }
|
|
480
|
-
end
|
|
481
|
-
|
|
482
|
-
# on dup/clone, we need to duplicate @names hash
|
|
483
|
-
def initialize_copy(other)
|
|
484
|
-
super
|
|
485
|
-
@names = other.names.dup
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
# on clear, we need to clear @names hash
|
|
489
|
-
def clear
|
|
490
|
-
super
|
|
491
|
-
@names.clear
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
def each
|
|
495
|
-
super do |k, v|
|
|
496
|
-
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
|
497
|
-
end
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
def to_hash
|
|
501
|
-
hash = {}
|
|
502
|
-
each { |k, v| hash[k] = v }
|
|
503
|
-
hash
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
def [](k)
|
|
507
|
-
super(k) || super(@names[k.downcase])
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
def []=(k, v)
|
|
511
|
-
canonical = k.downcase.freeze
|
|
512
|
-
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
|
|
513
|
-
@names[canonical] = k
|
|
514
|
-
super k, v
|
|
515
|
-
end
|
|
516
|
-
|
|
517
|
-
def delete(k)
|
|
518
|
-
canonical = k.downcase
|
|
519
|
-
result = super @names.delete(canonical)
|
|
520
|
-
result
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
def include?(k)
|
|
524
|
-
super || @names.include?(k.downcase)
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
alias_method :has_key?, :include?
|
|
528
|
-
alias_method :member?, :include?
|
|
529
|
-
alias_method :key?, :include?
|
|
530
|
-
|
|
531
|
-
def merge!(other)
|
|
532
|
-
other.each { |k, v| self[k] = v }
|
|
533
|
-
self
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
def merge(other)
|
|
537
|
-
hash = dup
|
|
538
|
-
hash.merge! other
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
def replace(other)
|
|
542
|
-
clear
|
|
543
|
-
other.each { |k, v| self[k] = v }
|
|
544
|
-
self
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
protected
|
|
548
|
-
def names
|
|
549
|
-
@names
|
|
550
|
-
end
|
|
551
|
-
end
|
|
552
|
-
|
|
553
492
|
# Every standard HTTP code mapped to the appropriate message.
|
|
554
493
|
# Generated with:
|
|
555
|
-
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv
|
|
556
|
-
# ruby -
|
|
557
|
-
#
|
|
494
|
+
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
|
|
495
|
+
# | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
|
|
496
|
+
# .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
|
|
497
|
+
# .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
|
|
558
498
|
HTTP_STATUS_CODES = {
|
|
559
499
|
100 => 'Continue',
|
|
560
500
|
101 => 'Switching Protocols',
|
|
@@ -576,7 +516,6 @@ module Rack
|
|
|
576
516
|
303 => 'See Other',
|
|
577
517
|
304 => 'Not Modified',
|
|
578
518
|
305 => 'Use Proxy',
|
|
579
|
-
306 => '(Unused)',
|
|
580
519
|
307 => 'Temporary Redirect',
|
|
581
520
|
308 => 'Permanent Redirect',
|
|
582
521
|
400 => 'Bad Request',
|
|
@@ -592,13 +531,13 @@ module Rack
|
|
|
592
531
|
410 => 'Gone',
|
|
593
532
|
411 => 'Length Required',
|
|
594
533
|
412 => 'Precondition Failed',
|
|
595
|
-
413 => '
|
|
534
|
+
413 => 'Content Too Large',
|
|
596
535
|
414 => 'URI Too Long',
|
|
597
536
|
415 => 'Unsupported Media Type',
|
|
598
537
|
416 => 'Range Not Satisfiable',
|
|
599
538
|
417 => 'Expectation Failed',
|
|
600
539
|
421 => 'Misdirected Request',
|
|
601
|
-
422 => 'Unprocessable
|
|
540
|
+
422 => 'Unprocessable Content',
|
|
602
541
|
423 => 'Locked',
|
|
603
542
|
424 => 'Failed Dependency',
|
|
604
543
|
425 => 'Too Early',
|
|
@@ -606,7 +545,7 @@ module Rack
|
|
|
606
545
|
428 => 'Precondition Required',
|
|
607
546
|
429 => 'Too Many Requests',
|
|
608
547
|
431 => 'Request Header Fields Too Large',
|
|
609
|
-
451 => 'Unavailable
|
|
548
|
+
451 => 'Unavailable For Legal Reasons',
|
|
610
549
|
500 => 'Internal Server Error',
|
|
611
550
|
501 => 'Not Implemented',
|
|
612
551
|
502 => 'Bad Gateway',
|
|
@@ -616,8 +555,6 @@ module Rack
|
|
|
616
555
|
506 => 'Variant Also Negotiates',
|
|
617
556
|
507 => 'Insufficient Storage',
|
|
618
557
|
508 => 'Loop Detected',
|
|
619
|
-
509 => 'Bandwidth Limit Exceeded',
|
|
620
|
-
510 => 'Not Extended',
|
|
621
558
|
511 => 'Network Authentication Required'
|
|
622
559
|
}
|
|
623
560
|
|
|
@@ -625,12 +562,34 @@ module Rack
|
|
|
625
562
|
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
|
626
563
|
|
|
627
564
|
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
|
628
|
-
[message.downcase.gsub(/\s
|
|
565
|
+
[message.downcase.gsub(/\s|-/, '_').to_sym, code]
|
|
629
566
|
}.flatten]
|
|
630
567
|
|
|
568
|
+
OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
|
|
569
|
+
payload_too_large: 413,
|
|
570
|
+
unprocessable_entity: 422,
|
|
571
|
+
bandwidth_limit_exceeded: 509,
|
|
572
|
+
not_extended: 510
|
|
573
|
+
}.freeze
|
|
574
|
+
private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
|
|
575
|
+
|
|
576
|
+
OBSOLETE_SYMBOL_MAPPINGS = {
|
|
577
|
+
payload_too_large: :content_too_large,
|
|
578
|
+
unprocessable_entity: :unprocessable_content
|
|
579
|
+
}.freeze
|
|
580
|
+
private_constant :OBSOLETE_SYMBOL_MAPPINGS
|
|
581
|
+
|
|
631
582
|
def status_code(status)
|
|
632
583
|
if status.is_a?(Symbol)
|
|
633
|
-
SYMBOL_TO_STATUS_CODE.fetch(status)
|
|
584
|
+
SYMBOL_TO_STATUS_CODE.fetch(status) do
|
|
585
|
+
fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
|
|
586
|
+
message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
|
|
587
|
+
if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
|
|
588
|
+
message = "#{message} Please use #{canonical_symbol.inspect} instead."
|
|
589
|
+
end
|
|
590
|
+
warn message, uplevel: 3
|
|
591
|
+
fallback_code
|
|
592
|
+
end
|
|
634
593
|
else
|
|
635
594
|
status.to_i
|
|
636
595
|
end
|