rack 2.1.0 → 3.1.0
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 +377 -16
- data/CONTRIBUTING.md +144 -0
- data/MIT-LICENSE +1 -1
- data/README.md +328 -0
- data/SPEC.rdoc +365 -0
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +2 -2
- data/lib/rack/auth/basic.rb +4 -7
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +34 -12
- data/lib/rack/builder.rb +162 -59
- data/lib/rack/cascade.rb +24 -10
- data/lib/rack/common_logger.rb +43 -28
- data/lib/rack/conditional_get.rb +30 -25
- data/lib/rack/constants.rb +66 -0
- data/lib/rack/content_length.rb +10 -16
- data/lib/rack/content_type.rb +9 -7
- data/lib/rack/deflater.rb +78 -50
- data/lib/rack/directory.rb +86 -63
- data/lib/rack/etag.rb +14 -22
- data/lib/rack/events.rb +18 -17
- data/lib/rack/files.rb +99 -61
- data/lib/rack/head.rb +8 -9
- data/lib/rack/headers.rb +238 -0
- data/lib/rack/lint.rb +868 -642
- data/lib/rack/lock.rb +2 -6
- data/lib/rack/logger.rb +3 -0
- data/lib/rack/media_type.rb +9 -4
- data/lib/rack/method_override.rb +6 -2
- data/lib/rack/mime.rb +14 -5
- data/lib/rack/mock.rb +1 -253
- data/lib/rack/mock_request.rb +171 -0
- data/lib/rack/mock_response.rb +124 -0
- data/lib/rack/multipart/generator.rb +15 -8
- data/lib/rack/multipart/parser.rb +238 -107
- data/lib/rack/multipart/uploaded_file.rb +17 -7
- data/lib/rack/multipart.rb +54 -42
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +87 -105
- data/lib/rack/recursive.rb +3 -1
- data/lib/rack/reloader.rb +0 -4
- data/lib/rack/request.rb +366 -135
- data/lib/rack/response.rb +186 -68
- data/lib/rack/rewindable_input.rb +24 -6
- data/lib/rack/runtime.rb +8 -7
- data/lib/rack/sendfile.rb +29 -27
- data/lib/rack/show_exceptions.rb +27 -12
- data/lib/rack/show_status.rb +21 -13
- data/lib/rack/static.rb +19 -12
- data/lib/rack/tempfile_reaper.rb +14 -5
- data/lib/rack/urlmap.rb +5 -6
- data/lib/rack/utils.rb +274 -260
- data/lib/rack/version.rb +21 -0
- data/lib/rack.rb +18 -103
- metadata +25 -52
- data/README.rdoc +0 -262
- data/Rakefile +0 -123
- data/SPEC +0 -263
- 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 -92
- data/lib/rack/core_ext/regexp.rb +0 -14
- data/lib/rack/file.rb +0 -8
- data/lib/rack/handler/cgi.rb +0 -62
- data/lib/rack/handler/fastcgi.rb +0 -102
- data/lib/rack/handler/lsws.rb +0 -63
- data/lib/rack/handler/scgi.rb +0 -73
- data/lib/rack/handler/thin.rb +0 -38
- data/lib/rack/handler/webrick.rb +0 -122
- data/lib/rack/handler.rb +0 -104
- data/lib/rack/lobster.rb +0 -72
- data/lib/rack/server.rb +0 -467
- data/lib/rack/session/abstract/id.rb +0 -528
- data/lib/rack/session/cookie.rb +0 -205
- data/lib/rack/session/memcache.rb +0 -10
- data/lib/rack/session/pool.rb +0 -85
- data/rack.gemspec +0 -44
data/lib/rack/utils.rb
CHANGED
@@ -5,20 +5,22 @@ require 'uri'
|
|
5
5
|
require 'fileutils'
|
6
6
|
require 'set'
|
7
7
|
require 'tempfile'
|
8
|
-
require 'rack/query_parser'
|
9
8
|
require 'time'
|
9
|
+
require 'cgi/escape'
|
10
10
|
|
11
|
-
require_relative '
|
11
|
+
require_relative 'query_parser'
|
12
|
+
require_relative 'mime'
|
13
|
+
require_relative 'headers'
|
14
|
+
require_relative 'constants'
|
12
15
|
|
13
16
|
module Rack
|
14
17
|
# Rack::Utils contains a grab-bag of useful methods for writing web
|
15
18
|
# applications adopted from all kinds of Ruby libraries.
|
16
19
|
|
17
20
|
module Utils
|
18
|
-
using ::Rack::RegexpExtensions
|
19
|
-
|
20
21
|
ParameterTypeError = QueryParser::ParameterTypeError
|
21
22
|
InvalidParameterError = QueryParser::InvalidParameterError
|
23
|
+
ParamsTooDeepError = QueryParser::ParamsTooDeepError
|
22
24
|
DEFAULT_SEP = QueryParser::DEFAULT_SEP
|
23
25
|
COMMON_SEP = QueryParser::COMMON_SEP
|
24
26
|
KeySpaceConstrainedParams = QueryParser::Params
|
@@ -26,46 +28,55 @@ module Rack
|
|
26
28
|
class << self
|
27
29
|
attr_accessor :default_query_parser
|
28
30
|
end
|
29
|
-
# The default
|
30
|
-
# This helps prevent a rogue client from
|
31
|
-
|
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)
|
35
|
+
|
36
|
+
module_function
|
32
37
|
|
33
38
|
# URI escapes. (CGI style space to +)
|
34
39
|
def escape(s)
|
35
40
|
URI.encode_www_form_component(s)
|
36
41
|
end
|
37
|
-
module_function :escape
|
38
42
|
|
39
43
|
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
|
40
44
|
# true URI escaping.
|
41
45
|
def escape_path(s)
|
42
46
|
::URI::DEFAULT_PARSER.escape s
|
43
47
|
end
|
44
|
-
module_function :escape_path
|
45
48
|
|
46
49
|
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
|
47
50
|
# unescaping query parameters or form components.
|
48
51
|
def unescape_path(s)
|
49
52
|
::URI::DEFAULT_PARSER.unescape s
|
50
53
|
end
|
51
|
-
module_function :unescape_path
|
52
|
-
|
53
54
|
|
54
55
|
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
|
55
56
|
# target encoding of the string returned, and it defaults to UTF-8
|
56
57
|
def unescape(s, encoding = Encoding::UTF_8)
|
57
58
|
URI.decode_www_form_component(s, encoding)
|
58
59
|
end
|
59
|
-
module_function :unescape
|
60
60
|
|
61
61
|
class << self
|
62
|
-
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=
|
63
70
|
end
|
64
71
|
|
65
|
-
# The maximum number of parts a request can contain. Accepting too
|
66
|
-
# 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.
|
67
74
|
# Set to `0` for no limit.
|
68
|
-
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
|
69
80
|
|
70
81
|
def self.param_depth_limit
|
71
82
|
default_query_parser.param_depth_limit
|
@@ -75,34 +86,25 @@ module Rack
|
|
75
86
|
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
|
76
87
|
end
|
77
88
|
|
78
|
-
def self.key_space_limit
|
79
|
-
default_query_parser.key_space_limit
|
80
|
-
end
|
81
|
-
|
82
|
-
def self.key_space_limit=(v)
|
83
|
-
self.default_query_parser = self.default_query_parser.new_space_limit(v)
|
84
|
-
end
|
85
|
-
|
86
89
|
if defined?(Process::CLOCK_MONOTONIC)
|
87
90
|
def clock_time
|
88
91
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
89
92
|
end
|
90
93
|
else
|
94
|
+
# :nocov:
|
91
95
|
def clock_time
|
92
96
|
Time.now.to_f
|
93
97
|
end
|
98
|
+
# :nocov:
|
94
99
|
end
|
95
|
-
module_function :clock_time
|
96
100
|
|
97
101
|
def parse_query(qs, d = nil, &unescaper)
|
98
102
|
Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
|
99
103
|
end
|
100
|
-
module_function :parse_query
|
101
104
|
|
102
105
|
def parse_nested_query(qs, d = nil)
|
103
106
|
Rack::Utils.default_query_parser.parse_nested_query(qs, d)
|
104
107
|
end
|
105
|
-
module_function :parse_nested_query
|
106
108
|
|
107
109
|
def build_query(params)
|
108
110
|
params.map { |k, v|
|
@@ -113,7 +115,6 @@ module Rack
|
|
113
115
|
end
|
114
116
|
}.join("&")
|
115
117
|
end
|
116
|
-
module_function :build_query
|
117
118
|
|
118
119
|
def build_nested_query(value, prefix = nil)
|
119
120
|
case value
|
@@ -123,20 +124,19 @@ module Rack
|
|
123
124
|
}.join("&")
|
124
125
|
when Hash
|
125
126
|
value.map { |k, v|
|
126
|
-
build_nested_query(v, prefix ? "#{prefix}[#{
|
127
|
+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
127
128
|
}.delete_if(&:empty?).join('&')
|
128
129
|
when nil
|
129
|
-
prefix
|
130
|
+
escape(prefix)
|
130
131
|
else
|
131
132
|
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
132
|
-
"#{prefix}=#{escape(value)}"
|
133
|
+
"#{escape(prefix)}=#{escape(value)}"
|
133
134
|
end
|
134
135
|
end
|
135
|
-
module_function :build_nested_query
|
136
136
|
|
137
137
|
def q_values(q_value_header)
|
138
|
-
q_value_header.to_s.split(
|
139
|
-
value, parameters = part.split(
|
138
|
+
q_value_header.to_s.split(',').map do |part|
|
139
|
+
value, parameters = part.split(';', 2).map(&:strip)
|
140
140
|
quality = 1.0
|
141
141
|
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
|
142
142
|
quality = md[1].to_f
|
@@ -144,8 +144,25 @@ module Rack
|
|
144
144
|
[value, quality]
|
145
145
|
end
|
146
146
|
end
|
147
|
-
module_function :q_values
|
148
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
|
+
|
162
|
+
# Return best accept value to use, based on the algorithm
|
163
|
+
# in RFC 2616 Section 14. If there are multiple best
|
164
|
+
# matches (same specificity and quality), the value returned
|
165
|
+
# is arbitrary.
|
149
166
|
def best_q_match(q_value_header, available_mimes)
|
150
167
|
values = q_values(q_value_header)
|
151
168
|
|
@@ -156,41 +173,32 @@ module Rack
|
|
156
173
|
end.compact.sort_by do |match, quality|
|
157
174
|
(match.split('/', 2).count('*') * -10) + quality
|
158
175
|
end.last
|
159
|
-
matches
|
176
|
+
matches&.first
|
160
177
|
end
|
161
|
-
module_function :best_q_match
|
162
|
-
|
163
|
-
ESCAPE_HTML = {
|
164
|
-
"&" => "&",
|
165
|
-
"<" => "<",
|
166
|
-
">" => ">",
|
167
|
-
"'" => "'",
|
168
|
-
'"' => """,
|
169
|
-
"/" => "/"
|
170
|
-
}
|
171
|
-
|
172
|
-
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
|
173
178
|
|
174
179
|
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
175
|
-
|
176
|
-
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
|
177
|
-
end
|
178
|
-
module_function :escape_html
|
180
|
+
define_method(:escape_html, CGI.method(:escapeHTML))
|
179
181
|
|
180
182
|
def select_best_encoding(available_encodings, accept_encoding)
|
181
183
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
182
184
|
|
183
|
-
expanded_accept_encoding =
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
185
|
+
expanded_accept_encoding = []
|
186
|
+
|
187
|
+
accept_encoding.each do |m, q|
|
188
|
+
preference = available_encodings.index(m) || available_encodings.size
|
189
|
+
|
190
|
+
if m == "*"
|
191
|
+
(available_encodings - accept_encoding.map(&:first)).each do |m2|
|
192
|
+
expanded_accept_encoding << [m2, q, preference]
|
190
193
|
end
|
194
|
+
else
|
195
|
+
expanded_accept_encoding << [m, q, preference]
|
191
196
|
end
|
197
|
+
end
|
192
198
|
|
193
|
-
encoding_candidates = expanded_accept_encoding
|
199
|
+
encoding_candidates = expanded_accept_encoding
|
200
|
+
.sort_by { |_, q, p| [-q, p] }
|
201
|
+
.map!(&:first)
|
194
202
|
|
195
203
|
unless encoding_candidates.include?("identity")
|
196
204
|
encoding_candidates.push("identity")
|
@@ -202,161 +210,215 @@ module Rack
|
|
202
210
|
|
203
211
|
(encoding_candidates & available_encodings)[0]
|
204
212
|
end
|
205
|
-
module_function :select_best_encoding
|
206
213
|
|
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
|
226
|
+
|
227
|
+
value.split(/; */n).each_with_object({}) do |cookie, cookies|
|
228
|
+
next if cookie.empty?
|
229
|
+
key, value = cookie.split('=', 2)
|
230
|
+
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
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
|
+
#
|
207
243
|
def parse_cookies(env)
|
208
244
|
parse_cookies_header env[HTTP_COOKIE]
|
209
245
|
end
|
210
|
-
module_function :parse_cookies
|
211
246
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
220
259
|
end
|
221
|
-
module_function :parse_cookies_header
|
222
260
|
|
223
|
-
|
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)
|
224
285
|
case value
|
225
286
|
when Hash
|
287
|
+
key = escape_cookie_key(key) unless value[:escape_key] == false
|
226
288
|
domain = "; domain=#{value[:domain]}" if value[:domain]
|
227
289
|
path = "; path=#{value[:path]}" if value[:path]
|
228
290
|
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
229
291
|
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
230
292
|
secure = "; secure" if value[:secure]
|
231
|
-
httponly = ";
|
293
|
+
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
232
294
|
same_site =
|
233
295
|
case value[:same_site]
|
234
296
|
when false, nil
|
235
297
|
nil
|
236
298
|
when :none, 'None', :None
|
237
|
-
';
|
299
|
+
'; samesite=none'
|
238
300
|
when :lax, 'Lax', :Lax
|
239
|
-
';
|
301
|
+
'; samesite=lax'
|
240
302
|
when true, :strict, 'Strict', :Strict
|
241
|
-
';
|
303
|
+
'; samesite=strict'
|
242
304
|
else
|
243
|
-
raise ArgumentError, "Invalid
|
305
|
+
raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
|
244
306
|
end
|
307
|
+
partitioned = "; partitioned" if value[:partitioned]
|
245
308
|
value = value[:value]
|
309
|
+
else
|
310
|
+
key = escape_cookie_key(key)
|
246
311
|
end
|
312
|
+
|
247
313
|
value = [value] unless Array === value
|
248
314
|
|
249
|
-
|
250
|
-
"#{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
|
251
318
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
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
|
259
334
|
else
|
260
|
-
|
335
|
+
headers[SET_COOKIE] = set_cookie_header(key, value)
|
261
336
|
end
|
262
337
|
end
|
263
|
-
module_function :add_cookie_to_header
|
264
338
|
|
265
|
-
|
266
|
-
|
267
|
-
|
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: ''))
|
268
355
|
end
|
269
|
-
module_function :set_cookie_header!
|
270
|
-
|
271
|
-
def make_delete_cookie_header(header, key, value)
|
272
|
-
case header
|
273
|
-
when nil, ''
|
274
|
-
cookies = []
|
275
|
-
when String
|
276
|
-
cookies = header.split("\n")
|
277
|
-
when Array
|
278
|
-
cookies = header
|
279
|
-
end
|
280
|
-
|
281
|
-
regexp = if value[:domain]
|
282
|
-
/\A#{escape(key)}=.*domain=#{value[:domain]}/
|
283
|
-
elsif value[:path]
|
284
|
-
/\A#{escape(key)}=.*path=#{value[:path]}/
|
285
|
-
else
|
286
|
-
/\A#{escape(key)}=/
|
287
|
-
end
|
288
356
|
|
289
|
-
|
290
|
-
|
291
|
-
cookies.join("\n")
|
292
|
-
end
|
293
|
-
module_function :make_delete_cookie_header
|
357
|
+
def delete_cookie_header!(headers, key, value = {})
|
358
|
+
headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
|
294
359
|
|
295
|
-
|
296
|
-
header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
|
297
|
-
nil
|
360
|
+
return nil
|
298
361
|
end
|
299
|
-
module_function :delete_cookie_header!
|
300
362
|
|
301
|
-
#
|
302
|
-
#
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
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
|
310
388
|
|
389
|
+
return header
|
311
390
|
end
|
312
|
-
module_function :add_remove_cookie_to_header
|
313
391
|
|
314
392
|
def rfc2822(time)
|
315
393
|
time.rfc2822
|
316
394
|
end
|
317
|
-
module_function :rfc2822
|
318
|
-
|
319
|
-
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
|
320
|
-
# of '% %b %Y'.
|
321
|
-
# It assumes that the time is in GMT to comply to the RFC 2109.
|
322
|
-
#
|
323
|
-
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
|
324
|
-
# that I'm certain someone implemented only that option.
|
325
|
-
# Do not use %a and %b from Time.strptime, it would use localized names for
|
326
|
-
# weekday and month.
|
327
|
-
#
|
328
|
-
def rfc2109(time)
|
329
|
-
wday = Time::RFC2822_DAY_NAME[time.wday]
|
330
|
-
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
|
331
|
-
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
|
332
|
-
end
|
333
|
-
module_function :rfc2109
|
334
395
|
|
335
396
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
336
397
|
# Returns nil if the header is missing or syntactically invalid.
|
337
398
|
# Returns an empty array if none of the ranges are satisfiable.
|
338
399
|
def byte_ranges(env, size)
|
339
|
-
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
|
340
400
|
get_byte_ranges env['HTTP_RANGE'], size
|
341
401
|
end
|
342
|
-
module_function :byte_ranges
|
343
402
|
|
344
403
|
def get_byte_ranges(http_range, size)
|
345
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?
|
346
407
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
347
408
|
ranges = []
|
348
409
|
$1.split(/,\s*/).each do |range_spec|
|
349
|
-
return nil
|
350
|
-
|
351
|
-
|
352
|
-
|
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?
|
353
415
|
# suffix-byte-range-spec, represents trailing suffix of file
|
354
416
|
r0 = size - r1.to_i
|
355
417
|
r0 = 0 if r0 < 0
|
356
418
|
r1 = size - 1
|
357
419
|
else
|
358
420
|
r0 = r0.to_i
|
359
|
-
if r1.
|
421
|
+
if r1.nil?
|
360
422
|
r1 = size - 1
|
361
423
|
else
|
362
424
|
r1 = r1.to_i
|
@@ -366,26 +428,37 @@ module Rack
|
|
366
428
|
end
|
367
429
|
ranges << (r0..r1) if r0 <= r1
|
368
430
|
end
|
431
|
+
|
432
|
+
return [] if ranges.map(&:size).sum > size
|
433
|
+
|
369
434
|
ranges
|
370
435
|
end
|
371
|
-
module_function :get_byte_ranges
|
372
436
|
|
373
|
-
#
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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
|
381
454
|
|
382
|
-
|
455
|
+
l = a.unpack("C*")
|
383
456
|
|
384
|
-
|
385
|
-
|
386
|
-
|
457
|
+
r, i = 0, -1
|
458
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
459
|
+
r == 0
|
460
|
+
end
|
387
461
|
end
|
388
|
-
module_function :secure_compare
|
389
462
|
|
390
463
|
# Context allows the use of a compatible middleware at different points
|
391
464
|
# in a request handling stack. A compatible middleware must define
|
@@ -413,87 +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 initialize(hash = {})
|
422
|
-
super()
|
423
|
-
@names = {}
|
424
|
-
hash.each { |k, v| self[k] = v }
|
425
|
-
end
|
426
|
-
|
427
|
-
# on dup/clone, we need to duplicate @names hash
|
428
|
-
def initialize_copy(other)
|
429
|
-
super
|
430
|
-
@names = other.names.dup
|
431
|
-
end
|
432
|
-
|
433
|
-
def each
|
434
|
-
super do |k, v|
|
435
|
-
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
436
|
-
end
|
437
|
-
end
|
438
|
-
|
439
|
-
def to_hash
|
440
|
-
hash = {}
|
441
|
-
each { |k, v| hash[k] = v }
|
442
|
-
hash
|
443
|
-
end
|
444
|
-
|
445
|
-
def [](k)
|
446
|
-
super(k) || super(@names[k.downcase])
|
447
|
-
end
|
448
|
-
|
449
|
-
def []=(k, v)
|
450
|
-
canonical = k.downcase.freeze
|
451
|
-
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
|
452
|
-
@names[canonical] = k
|
453
|
-
super k, v
|
454
|
-
end
|
455
|
-
|
456
|
-
def delete(k)
|
457
|
-
canonical = k.downcase
|
458
|
-
result = super @names.delete(canonical)
|
459
|
-
result
|
460
|
-
end
|
461
|
-
|
462
|
-
def include?(k)
|
463
|
-
super || @names.include?(k.downcase)
|
464
|
-
end
|
465
|
-
|
466
|
-
alias_method :has_key?, :include?
|
467
|
-
alias_method :member?, :include?
|
468
|
-
alias_method :key?, :include?
|
469
|
-
|
470
|
-
def merge!(other)
|
471
|
-
other.each { |k, v| self[k] = v }
|
472
|
-
self
|
473
|
-
end
|
474
|
-
|
475
|
-
def merge(other)
|
476
|
-
hash = dup
|
477
|
-
hash.merge! other
|
478
|
-
end
|
479
|
-
|
480
|
-
def replace(other)
|
481
|
-
clear
|
482
|
-
other.each { |k, v| self[k] = v }
|
483
|
-
self
|
484
|
-
end
|
485
|
-
|
486
|
-
protected
|
487
|
-
def names
|
488
|
-
@names
|
489
|
-
end
|
490
|
-
end
|
491
|
-
|
492
489
|
# Every standard HTTP code mapped to the appropriate message.
|
493
490
|
# Generated with:
|
494
|
-
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv
|
495
|
-
# ruby -
|
496
|
-
#
|
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)"
|
497
495
|
HTTP_STATUS_CODES = {
|
498
496
|
100 => 'Continue',
|
499
497
|
101 => 'Switching Protocols',
|
@@ -515,7 +513,6 @@ module Rack
|
|
515
513
|
303 => 'See Other',
|
516
514
|
304 => 'Not Modified',
|
517
515
|
305 => 'Use Proxy',
|
518
|
-
306 => '(Unused)',
|
519
516
|
307 => 'Temporary Redirect',
|
520
517
|
308 => 'Permanent Redirect',
|
521
518
|
400 => 'Bad Request',
|
@@ -531,13 +528,13 @@ module Rack
|
|
531
528
|
410 => 'Gone',
|
532
529
|
411 => 'Length Required',
|
533
530
|
412 => 'Precondition Failed',
|
534
|
-
413 => '
|
531
|
+
413 => 'Content Too Large',
|
535
532
|
414 => 'URI Too Long',
|
536
533
|
415 => 'Unsupported Media Type',
|
537
534
|
416 => 'Range Not Satisfiable',
|
538
535
|
417 => 'Expectation Failed',
|
539
536
|
421 => 'Misdirected Request',
|
540
|
-
422 => 'Unprocessable
|
537
|
+
422 => 'Unprocessable Content',
|
541
538
|
423 => 'Locked',
|
542
539
|
424 => 'Failed Dependency',
|
543
540
|
425 => 'Too Early',
|
@@ -545,7 +542,7 @@ module Rack
|
|
545
542
|
428 => 'Precondition Required',
|
546
543
|
429 => 'Too Many Requests',
|
547
544
|
431 => 'Request Header Fields Too Large',
|
548
|
-
451 => 'Unavailable
|
545
|
+
451 => 'Unavailable For Legal Reasons',
|
549
546
|
500 => 'Internal Server Error',
|
550
547
|
501 => 'Not Implemented',
|
551
548
|
502 => 'Bad Gateway',
|
@@ -555,8 +552,6 @@ module Rack
|
|
555
552
|
506 => 'Variant Also Negotiates',
|
556
553
|
507 => 'Insufficient Storage',
|
557
554
|
508 => 'Loop Detected',
|
558
|
-
509 => 'Bandwidth Limit Exceeded',
|
559
|
-
510 => 'Not Extended',
|
560
555
|
511 => 'Network Authentication Required'
|
561
556
|
}
|
562
557
|
|
@@ -564,17 +559,38 @@ module Rack
|
|
564
559
|
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
565
560
|
|
566
561
|
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
567
|
-
[message.downcase.gsub(/\s
|
562
|
+
[message.downcase.gsub(/\s|-/, '_').to_sym, code]
|
568
563
|
}.flatten]
|
569
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
|
+
|
570
579
|
def status_code(status)
|
571
580
|
if status.is_a?(Symbol)
|
572
|
-
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
|
573
590
|
else
|
574
591
|
status.to_i
|
575
592
|
end
|
576
593
|
end
|
577
|
-
module_function :status_code
|
578
594
|
|
579
595
|
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
|
580
596
|
|
@@ -588,18 +604,16 @@ module Rack
|
|
588
604
|
part == '..' ? clean.pop : clean << part
|
589
605
|
end
|
590
606
|
|
591
|
-
|
592
|
-
|
593
|
-
|
607
|
+
clean_path = clean.join(::File::SEPARATOR)
|
608
|
+
clean_path.prepend("/") if parts.empty? || parts.first.empty?
|
609
|
+
clean_path
|
594
610
|
end
|
595
|
-
module_function :clean_path_info
|
596
611
|
|
597
612
|
NULL_BYTE = "\0"
|
598
613
|
|
599
614
|
def valid_path?(path)
|
600
615
|
path.valid_encoding? && !path.include?(NULL_BYTE)
|
601
616
|
end
|
602
|
-
module_function :valid_path?
|
603
617
|
|
604
618
|
end
|
605
619
|
end
|