rack 2.0.9.3 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +808 -0
- data/CONTRIBUTING.md +142 -0
- data/{COPYING → MIT-LICENSE} +4 -2
- data/README.md +293 -0
- data/SPEC.rdoc +340 -0
- data/lib/rack/auth/abstract/handler.rb +6 -2
- data/lib/rack/auth/abstract/request.rb +4 -2
- data/lib/rack/auth/basic.rb +7 -4
- data/lib/rack/auth/digest/md5.rb +1 -129
- data/lib/rack/auth/digest/nonce.rb +1 -51
- data/lib/rack/auth/digest/params.rb +1 -52
- data/lib/rack/auth/digest/request.rb +1 -41
- data/lib/rack/auth/digest.rb +256 -0
- data/lib/rack/body_proxy.rb +18 -15
- data/lib/rack/builder.rb +151 -40
- data/lib/rack/cascade.rb +30 -12
- data/lib/rack/chunked.rb +74 -23
- data/lib/rack/common_logger.rb +49 -36
- data/lib/rack/conditional_get.rb +33 -26
- data/lib/rack/config.rb +2 -0
- data/lib/rack/constants.rb +63 -0
- data/lib/rack/content_length.rb +13 -16
- data/lib/rack/content_type.rb +12 -8
- data/lib/rack/deflater.rb +84 -45
- data/lib/rack/directory.rb +90 -64
- data/lib/rack/etag.rb +17 -23
- data/lib/rack/events.rb +23 -20
- data/lib/rack/file.rb +5 -172
- data/lib/rack/files.rb +216 -0
- data/lib/rack/head.rb +10 -9
- data/lib/rack/headers.rb +154 -0
- data/lib/rack/lint.rb +786 -645
- data/lib/rack/lock.rb +4 -6
- data/lib/rack/logger.rb +4 -0
- data/lib/rack/media_type.rb +10 -5
- data/lib/rack/method_override.rb +8 -2
- data/lib/rack/mime.rb +17 -1
- data/lib/rack/mock.rb +2 -195
- data/lib/rack/mock_request.rb +166 -0
- data/lib/rack/mock_response.rb +126 -0
- data/lib/rack/multipart/generator.rb +21 -15
- data/lib/rack/multipart/parser.rb +161 -118
- data/lib/rack/multipart/uploaded_file.rb +19 -7
- data/lib/rack/multipart.rb +23 -41
- data/lib/rack/null_logger.rb +11 -0
- data/lib/rack/query_parser.rb +126 -65
- data/lib/rack/recursive.rb +9 -5
- data/lib/rack/reloader.rb +6 -4
- data/lib/rack/request.rb +331 -74
- data/lib/rack/response.rb +223 -70
- data/lib/rack/rewindable_input.rb +28 -8
- data/lib/rack/runtime.rb +11 -8
- data/lib/rack/sendfile.rb +42 -33
- data/lib/rack/show_exceptions.rb +35 -18
- data/lib/rack/show_status.rb +25 -15
- data/lib/rack/static.rb +30 -18
- data/lib/rack/tempfile_reaper.rb +16 -5
- data/lib/rack/urlmap.rb +14 -6
- data/lib/rack/utils.rb +268 -260
- data/lib/rack/version.rb +34 -0
- data/lib/rack.rb +15 -92
- metadata +44 -207
- data/HISTORY.md +0 -520
- data/README.rdoc +0 -316
- data/Rakefile +0 -116
- data/SPEC +0 -263
- data/bin/rackup +0 -4
- 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 -4
- data/example/protectedlobster.rb +0 -14
- data/example/protectedlobster.ru +0 -8
- data/lib/rack/handler/cgi.rb +0 -60
- data/lib/rack/handler/fastcgi.rb +0 -100
- data/lib/rack/handler/lsws.rb +0 -61
- data/lib/rack/handler/scgi.rb +0 -70
- data/lib/rack/handler/thin.rb +0 -36
- data/lib/rack/handler/webrick.rb +0 -120
- data/lib/rack/handler.rb +0 -99
- data/lib/rack/lobster.rb +0 -70
- data/lib/rack/server.rb +0 -395
- data/lib/rack/session/abstract/id.rb +0 -510
- data/lib/rack/session/cookie.rb +0 -204
- data/lib/rack/session/memcache.rb +0 -99
- data/lib/rack/session/pool.rb +0 -83
- data/rack.gemspec +0 -34
- data/test/builder/an_underscore_app.rb +0 -5
- data/test/builder/anything.rb +0 -5
- data/test/builder/comment.ru +0 -4
- data/test/builder/end.ru +0 -5
- data/test/builder/line.ru +0 -1
- data/test/builder/options.ru +0 -2
- data/test/cgi/assets/folder/test.js +0 -1
- data/test/cgi/assets/fonts/font.eot +0 -1
- data/test/cgi/assets/images/image.png +0 -1
- data/test/cgi/assets/index.html +0 -1
- data/test/cgi/assets/javascripts/app.js +0 -1
- data/test/cgi/assets/stylesheets/app.css +0 -1
- data/test/cgi/lighttpd.conf +0 -26
- data/test/cgi/rackup_stub.rb +0 -6
- data/test/cgi/sample_rackup.ru +0 -5
- data/test/cgi/test +0 -9
- data/test/cgi/test+directory/test+file +0 -1
- data/test/cgi/test.fcgi +0 -9
- data/test/cgi/test.gz +0 -0
- data/test/cgi/test.ru +0 -5
- data/test/gemloader.rb +0 -10
- data/test/helper.rb +0 -34
- data/test/multipart/bad_robots +0 -259
- data/test/multipart/binary +0 -0
- data/test/multipart/content_type_and_no_filename +0 -6
- data/test/multipart/empty +0 -10
- data/test/multipart/fail_16384_nofile +0 -814
- data/test/multipart/file1.txt +0 -1
- data/test/multipart/filename_and_modification_param +0 -7
- data/test/multipart/filename_and_no_name +0 -6
- data/test/multipart/filename_with_encoded_words +0 -7
- data/test/multipart/filename_with_escaped_quotes +0 -6
- data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
- data/test/multipart/filename_with_null_byte +0 -7
- data/test/multipart/filename_with_percent_escaped_quotes +0 -6
- data/test/multipart/filename_with_single_quote +0 -7
- data/test/multipart/filename_with_unescaped_percentages +0 -6
- data/test/multipart/filename_with_unescaped_percentages2 +0 -6
- data/test/multipart/filename_with_unescaped_percentages3 +0 -6
- data/test/multipart/filename_with_unescaped_quotes +0 -6
- data/test/multipart/ie +0 -6
- data/test/multipart/invalid_character +0 -6
- data/test/multipart/mixed_files +0 -21
- data/test/multipart/nested +0 -10
- data/test/multipart/none +0 -9
- data/test/multipart/quoted +0 -15
- data/test/multipart/rack-logo.png +0 -0
- data/test/multipart/semicolon +0 -6
- data/test/multipart/text +0 -15
- data/test/multipart/three_files_three_fields +0 -31
- data/test/multipart/unity3d_wwwform +0 -11
- data/test/multipart/webkit +0 -32
- data/test/rackup/config.ru +0 -31
- data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
- data/test/spec_auth_basic.rb +0 -89
- data/test/spec_auth_digest.rb +0 -260
- data/test/spec_body_proxy.rb +0 -85
- data/test/spec_builder.rb +0 -233
- data/test/spec_cascade.rb +0 -63
- data/test/spec_cgi.rb +0 -84
- data/test/spec_chunked.rb +0 -103
- data/test/spec_common_logger.rb +0 -107
- data/test/spec_conditional_get.rb +0 -103
- data/test/spec_config.rb +0 -23
- data/test/spec_content_length.rb +0 -86
- data/test/spec_content_type.rb +0 -46
- data/test/spec_deflater.rb +0 -375
- data/test/spec_directory.rb +0 -148
- data/test/spec_etag.rb +0 -108
- data/test/spec_events.rb +0 -133
- data/test/spec_fastcgi.rb +0 -85
- data/test/spec_file.rb +0 -264
- data/test/spec_handler.rb +0 -57
- data/test/spec_head.rb +0 -46
- data/test/spec_lint.rb +0 -520
- data/test/spec_lobster.rb +0 -59
- data/test/spec_lock.rb +0 -204
- data/test/spec_logger.rb +0 -24
- data/test/spec_media_type.rb +0 -42
- data/test/spec_method_override.rb +0 -110
- data/test/spec_mime.rb +0 -51
- data/test/spec_mock.rb +0 -359
- data/test/spec_multipart.rb +0 -721
- data/test/spec_null_logger.rb +0 -21
- data/test/spec_recursive.rb +0 -75
- data/test/spec_request.rb +0 -1423
- data/test/spec_response.rb +0 -528
- data/test/spec_rewindable_input.rb +0 -128
- data/test/spec_runtime.rb +0 -50
- data/test/spec_sendfile.rb +0 -125
- data/test/spec_server.rb +0 -193
- data/test/spec_session_abstract_id.rb +0 -31
- data/test/spec_session_abstract_session_hash.rb +0 -45
- data/test/spec_session_cookie.rb +0 -442
- data/test/spec_session_memcache.rb +0 -357
- data/test/spec_session_persisted_secure_secure_session_hash.rb +0 -73
- data/test/spec_session_pool.rb +0 -247
- data/test/spec_show_exceptions.rb +0 -93
- data/test/spec_show_status.rb +0 -104
- data/test/spec_static.rb +0 -184
- data/test/spec_tempfile_reaper.rb +0 -64
- data/test/spec_thin.rb +0 -96
- data/test/spec_urlmap.rb +0 -237
- data/test/spec_utils.rb +0 -742
- data/test/spec_version.rb +0 -11
- data/test/spec_webrick.rb +0 -206
- data/test/static/another/index.html +0 -1
- data/test/static/foo.html +0 -1
- data/test/static/index.html +0 -1
- data/test/testrequest.rb +0 -78
- data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/utils.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
2
4
|
require 'uri'
|
3
5
|
require 'fileutils'
|
4
6
|
require 'set'
|
5
7
|
require 'tempfile'
|
6
|
-
require 'rack/query_parser'
|
7
8
|
require 'time'
|
8
9
|
|
10
|
+
require_relative 'query_parser'
|
11
|
+
require_relative 'mime'
|
12
|
+
require_relative 'headers'
|
13
|
+
require_relative 'constants'
|
14
|
+
|
9
15
|
module Rack
|
10
16
|
# Rack::Utils contains a grab-bag of useful methods for writing web
|
11
17
|
# applications adopted from all kinds of Ruby libraries.
|
@@ -13,6 +19,7 @@ module Rack
|
|
13
19
|
module Utils
|
14
20
|
ParameterTypeError = QueryParser::ParameterTypeError
|
15
21
|
InvalidParameterError = QueryParser::InvalidParameterError
|
22
|
+
ParamsTooDeepError = QueryParser::ParamsTooDeepError
|
16
23
|
DEFAULT_SEP = QueryParser::DEFAULT_SEP
|
17
24
|
COMMON_SEP = QueryParser::COMMON_SEP
|
18
25
|
KeySpaceConstrainedParams = QueryParser::Params
|
@@ -20,57 +27,44 @@ module Rack
|
|
20
27
|
class << self
|
21
28
|
attr_accessor :default_query_parser
|
22
29
|
end
|
23
|
-
# The default
|
24
|
-
# This helps prevent a rogue client from
|
25
|
-
|
30
|
+
# The default amount of nesting to allowed by hash parameters.
|
31
|
+
# This helps prevent a rogue client from triggering a possible stack overflow
|
32
|
+
# when parsing parameters.
|
33
|
+
self.default_query_parser = QueryParser.make_default(32)
|
34
|
+
|
35
|
+
module_function
|
26
36
|
|
27
37
|
# URI escapes. (CGI style space to +)
|
28
38
|
def escape(s)
|
29
39
|
URI.encode_www_form_component(s)
|
30
40
|
end
|
31
|
-
module_function :escape
|
32
41
|
|
33
42
|
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
|
34
43
|
# true URI escaping.
|
35
44
|
def escape_path(s)
|
36
45
|
::URI::DEFAULT_PARSER.escape s
|
37
46
|
end
|
38
|
-
module_function :escape_path
|
39
47
|
|
40
48
|
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
|
41
49
|
# unescaping query parameters or form components.
|
42
50
|
def unescape_path(s)
|
43
51
|
::URI::DEFAULT_PARSER.unescape s
|
44
52
|
end
|
45
|
-
module_function :unescape_path
|
46
|
-
|
47
53
|
|
48
54
|
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
|
49
55
|
# target encoding of the string returned, and it defaults to UTF-8
|
50
56
|
def unescape(s, encoding = Encoding::UTF_8)
|
51
57
|
URI.decode_www_form_component(s, encoding)
|
52
58
|
end
|
53
|
-
module_function :unescape
|
54
59
|
|
55
60
|
class << self
|
56
|
-
attr_accessor :
|
57
|
-
|
58
|
-
attr_accessor :multipart_file_limit
|
59
|
-
|
60
|
-
# multipart_part_limit is the original name of multipart_file_limit, but
|
61
|
-
# the limit only counts parts with filenames.
|
62
|
-
alias multipart_part_limit multipart_file_limit
|
63
|
-
alias multipart_part_limit= multipart_file_limit=
|
61
|
+
attr_accessor :multipart_part_limit
|
64
62
|
end
|
65
63
|
|
66
|
-
# The maximum number of
|
67
|
-
#
|
64
|
+
# The maximum number of parts a request can contain. Accepting too many part
|
65
|
+
# can lead to the server running out of file handles.
|
68
66
|
# Set to `0` for no limit.
|
69
|
-
self.
|
70
|
-
|
71
|
-
# The maximum total number of parts a request can contain. Accepting too
|
72
|
-
# many can lead to excessive memory use and parsing time.
|
73
|
-
self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
|
67
|
+
self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
|
74
68
|
|
75
69
|
def self.param_depth_limit
|
76
70
|
default_query_parser.param_depth_limit
|
@@ -81,11 +75,12 @@ module Rack
|
|
81
75
|
end
|
82
76
|
|
83
77
|
def self.key_space_limit
|
84
|
-
|
78
|
+
warn("`Rack::Utils.key_space_limit` is deprecated as this value no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
|
79
|
+
65536
|
85
80
|
end
|
86
81
|
|
87
82
|
def self.key_space_limit=(v)
|
88
|
-
|
83
|
+
warn("`Rack::Utils.key_space_limit=` is deprecated and no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
|
89
84
|
end
|
90
85
|
|
91
86
|
if defined?(Process::CLOCK_MONOTONIC)
|
@@ -93,21 +88,20 @@ module Rack
|
|
93
88
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
94
89
|
end
|
95
90
|
else
|
91
|
+
# :nocov:
|
96
92
|
def clock_time
|
97
93
|
Time.now.to_f
|
98
94
|
end
|
95
|
+
# :nocov:
|
99
96
|
end
|
100
|
-
module_function :clock_time
|
101
97
|
|
102
98
|
def parse_query(qs, d = nil, &unescaper)
|
103
99
|
Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
|
104
100
|
end
|
105
|
-
module_function :parse_query
|
106
101
|
|
107
102
|
def parse_nested_query(qs, d = nil)
|
108
103
|
Rack::Utils.default_query_parser.parse_nested_query(qs, d)
|
109
104
|
end
|
110
|
-
module_function :parse_nested_query
|
111
105
|
|
112
106
|
def build_query(params)
|
113
107
|
params.map { |k, v|
|
@@ -118,7 +112,6 @@ module Rack
|
|
118
112
|
end
|
119
113
|
}.join("&")
|
120
114
|
end
|
121
|
-
module_function :build_query
|
122
115
|
|
123
116
|
def build_nested_query(value, prefix = nil)
|
124
117
|
case value
|
@@ -129,7 +122,7 @@ module Rack
|
|
129
122
|
when Hash
|
130
123
|
value.map { |k, v|
|
131
124
|
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
|
132
|
-
}.
|
125
|
+
}.delete_if(&:empty?).join('&')
|
133
126
|
when nil
|
134
127
|
prefix
|
135
128
|
else
|
@@ -137,20 +130,35 @@ module Rack
|
|
137
130
|
"#{prefix}=#{escape(value)}"
|
138
131
|
end
|
139
132
|
end
|
140
|
-
module_function :build_nested_query
|
141
133
|
|
142
134
|
def q_values(q_value_header)
|
143
135
|
q_value_header.to_s.split(/\s*,\s*/).map do |part|
|
144
136
|
value, parameters = part.split(/\s*;\s*/, 2)
|
145
137
|
quality = 1.0
|
146
|
-
if md = /\Aq=([\d.]+)/.match(parameters)
|
138
|
+
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
|
147
139
|
quality = md[1].to_f
|
148
140
|
end
|
149
141
|
[value, quality]
|
150
142
|
end
|
151
143
|
end
|
152
|
-
module_function :q_values
|
153
144
|
|
145
|
+
def forwarded_values(forwarded_header)
|
146
|
+
return nil unless forwarded_header
|
147
|
+
forwarded_header = forwarded_header.to_s.gsub("\n", ";")
|
148
|
+
|
149
|
+
forwarded_header.split(/\s*;\s*/).each_with_object({}) do |field, values|
|
150
|
+
field.split(/\s*,\s*/).each do |pair|
|
151
|
+
return nil unless pair =~ /\A\s*(by|for|host|proto)\s*=\s*"?([^"]+)"?\s*\Z/i
|
152
|
+
(values[$1.downcase.to_sym] ||= []) << $2
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
module_function :forwarded_values
|
157
|
+
|
158
|
+
# Return best accept value to use, based on the algorithm
|
159
|
+
# in RFC 2616 Section 14. If there are multiple best
|
160
|
+
# matches (same specificity and quality), the value returned
|
161
|
+
# is arbitrary.
|
154
162
|
def best_q_match(q_value_header, available_mimes)
|
155
163
|
values = q_values(q_value_header)
|
156
164
|
|
@@ -161,9 +169,8 @@ module Rack
|
|
161
169
|
end.compact.sort_by do |match, quality|
|
162
170
|
(match.split('/', 2).count('*') * -10) + quality
|
163
171
|
end.last
|
164
|
-
matches
|
172
|
+
matches&.first
|
165
173
|
end
|
166
|
-
module_function :best_q_match
|
167
174
|
|
168
175
|
ESCAPE_HTML = {
|
169
176
|
"&" => "&",
|
@@ -180,243 +187,293 @@ module Rack
|
|
180
187
|
def escape_html(string)
|
181
188
|
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
|
182
189
|
end
|
183
|
-
module_function :escape_html
|
184
190
|
|
185
191
|
def select_best_encoding(available_encodings, accept_encoding)
|
186
192
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
187
193
|
|
188
|
-
expanded_accept_encoding =
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
+
expanded_accept_encoding = []
|
195
|
+
|
196
|
+
accept_encoding.each do |m, q|
|
197
|
+
preference = available_encodings.index(m) || available_encodings.size
|
198
|
+
|
199
|
+
if m == "*"
|
200
|
+
(available_encodings - accept_encoding.map(&:first)).each do |m2|
|
201
|
+
expanded_accept_encoding << [m2, q, preference]
|
194
202
|
end
|
195
|
-
|
196
|
-
|
197
|
-
|
203
|
+
else
|
204
|
+
expanded_accept_encoding << [m, q, preference]
|
205
|
+
end
|
206
|
+
end
|
198
207
|
|
199
|
-
encoding_candidates = expanded_accept_encoding
|
208
|
+
encoding_candidates = expanded_accept_encoding
|
209
|
+
.sort_by { |_, q, p| [-q, p] }
|
210
|
+
.map!(&:first)
|
200
211
|
|
201
212
|
unless encoding_candidates.include?("identity")
|
202
213
|
encoding_candidates.push("identity")
|
203
214
|
end
|
204
215
|
|
205
|
-
expanded_accept_encoding.each
|
216
|
+
expanded_accept_encoding.each do |m, q|
|
206
217
|
encoding_candidates.delete(m) if q == 0.0
|
207
|
-
|
218
|
+
end
|
208
219
|
|
209
|
-
|
220
|
+
(encoding_candidates & available_encodings)[0]
|
210
221
|
end
|
211
|
-
module_function :select_best_encoding
|
212
222
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
223
|
+
# :call-seq:
|
224
|
+
# parse_cookies_header(value) -> hash
|
225
|
+
#
|
226
|
+
# Parse cookies from the provided header +value+ according to RFC6265. The
|
227
|
+
# syntax for cookie headers only supports semicolons. Returns a map of
|
228
|
+
# cookie +key+ to cookie +value+.
|
229
|
+
#
|
230
|
+
# parse_cookies_header('myname=myvalue; max-age=0')
|
231
|
+
# # => {"myname"=>"myvalue", "max-age"=>"0"}
|
232
|
+
#
|
233
|
+
def parse_cookies_header(value)
|
234
|
+
return {} unless value
|
217
235
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
# attributes (e.g., Domain) is unspecified.
|
224
|
-
cookies = parse_query(header, ';,') { |s| unescape(s) rescue s }
|
225
|
-
cookies.each_with_object({}) { |(k,v), hash| hash[k] = Array === v ? v.first : v }
|
236
|
+
value.split(/; */n).each_with_object({}) do |cookie, cookies|
|
237
|
+
next if cookie.empty?
|
238
|
+
key, value = cookie.split('=', 2)
|
239
|
+
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
240
|
+
end
|
226
241
|
end
|
227
|
-
module_function :parse_cookies_header
|
228
242
|
|
229
243
|
def add_cookie_to_header(header, key, value)
|
244
|
+
warn("add_cookie_to_header is deprecated and will be removed in Rack 3.1", uplevel: 1)
|
245
|
+
|
246
|
+
case header
|
247
|
+
when nil, ''
|
248
|
+
return set_cookie_header(key, value)
|
249
|
+
when String
|
250
|
+
[header, set_cookie_header(key, value)]
|
251
|
+
when Array
|
252
|
+
header + [set_cookie_header(key, value)]
|
253
|
+
else
|
254
|
+
raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# :call-seq:
|
259
|
+
# parse_cookies(env) -> hash
|
260
|
+
#
|
261
|
+
# Parse cookies from the provided request environment using
|
262
|
+
# parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
|
263
|
+
#
|
264
|
+
# parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
|
265
|
+
# # => {'myname' => 'myvalue'}
|
266
|
+
#
|
267
|
+
def parse_cookies(env)
|
268
|
+
parse_cookies_header env[HTTP_COOKIE]
|
269
|
+
end
|
270
|
+
|
271
|
+
# :call-seq:
|
272
|
+
# set_cookie_header(key, value) -> encoded string
|
273
|
+
#
|
274
|
+
# Generate an encoded string using the provided +key+ and +value+ suitable
|
275
|
+
# for the +set-cookie+ header according to RFC6265. The +value+ may be an
|
276
|
+
# instance of either +String+ or +Hash+.
|
277
|
+
#
|
278
|
+
# If the cookie +value+ is an instance of +Hash+, it considers the following
|
279
|
+
# cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
|
280
|
+
# of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
|
281
|
+
# details about the interpretation of these fields, consult
|
282
|
+
# [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
|
283
|
+
#
|
284
|
+
# An extra cookie attribute +escape_key+ can be provided to control whether
|
285
|
+
# or not the cookie key is URL encoded. If explicitly set to +false+, the
|
286
|
+
# cookie key name will not be url encoded (escaped). The default is +true+.
|
287
|
+
#
|
288
|
+
# set_cookie_header("myname", "myvalue")
|
289
|
+
# # => "myname=myvalue"
|
290
|
+
#
|
291
|
+
# set_cookie_header("myname", {value: "myvalue", max_age: 10})
|
292
|
+
# # => "myname=myvalue; max-age=10"
|
293
|
+
#
|
294
|
+
def set_cookie_header(key, value)
|
230
295
|
case value
|
231
296
|
when Hash
|
297
|
+
key = escape(key) unless value[:escape_key] == false
|
232
298
|
domain = "; domain=#{value[:domain]}" if value[:domain]
|
233
299
|
path = "; path=#{value[:path]}" if value[:path]
|
234
300
|
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
235
|
-
|
236
|
-
# only are there contradicting RFCs and examples within RFC text, but
|
237
|
-
# there are also numerous conflicting names of fields and partially
|
238
|
-
# cross-applicable specifications.
|
239
|
-
#
|
240
|
-
# These are best described in RFC 2616 3.3.1. This RFC text also
|
241
|
-
# specifies that RFC 822 as updated by RFC 1123 is preferred. That is a
|
242
|
-
# fixed length format with space-date delimited fields.
|
243
|
-
#
|
244
|
-
# See also RFC 1123 section 5.2.14.
|
245
|
-
#
|
246
|
-
# RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined
|
247
|
-
# in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote
|
248
|
-
# the space delimited format. These formats are compliant with RFC 2822.
|
249
|
-
#
|
250
|
-
# For reference, all involved RFCs are:
|
251
|
-
# RFC 822
|
252
|
-
# RFC 1123
|
253
|
-
# RFC 2109
|
254
|
-
# RFC 2616
|
255
|
-
# RFC 2822
|
256
|
-
# RFC 2965
|
257
|
-
# RFC 6265
|
258
|
-
expires = "; expires=" +
|
259
|
-
rfc2822(value[:expires].clone.gmtime) if value[:expires]
|
301
|
+
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
260
302
|
secure = "; secure" if value[:secure]
|
261
|
-
httponly = ";
|
303
|
+
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
262
304
|
same_site =
|
263
305
|
case value[:same_site]
|
264
306
|
when false, nil
|
265
307
|
nil
|
266
308
|
when :none, 'None', :None
|
267
|
-
'; SameSite=None'
|
309
|
+
'; SameSite=None'
|
268
310
|
when :lax, 'Lax', :Lax
|
269
|
-
'; SameSite=Lax'
|
311
|
+
'; SameSite=Lax'
|
270
312
|
when true, :strict, 'Strict', :Strict
|
271
|
-
'; SameSite=Strict'
|
313
|
+
'; SameSite=Strict'
|
272
314
|
else
|
273
315
|
raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
|
274
316
|
end
|
275
317
|
value = value[:value]
|
318
|
+
else
|
319
|
+
key = escape(key)
|
276
320
|
end
|
321
|
+
|
277
322
|
value = [value] unless Array === value
|
278
323
|
|
279
|
-
|
324
|
+
return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
|
280
325
|
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
|
326
|
+
end
|
281
327
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
328
|
+
# :call-seq:
|
329
|
+
# set_cookie_header!(headers, key, value) -> header value
|
330
|
+
#
|
331
|
+
# Append a cookie in the specified headers with the given cookie +key+ and
|
332
|
+
# +value+ using set_cookie_header.
|
333
|
+
#
|
334
|
+
# If the headers already contains a +set-cookie+ key, it will be converted
|
335
|
+
# to an +Array+ if not already, and appended to.
|
336
|
+
def set_cookie_header!(headers, key, value)
|
337
|
+
if header = headers[SET_COOKIE]
|
338
|
+
if header.is_a?(Array)
|
339
|
+
header << set_cookie_header(key, value)
|
340
|
+
else
|
341
|
+
headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
|
342
|
+
end
|
289
343
|
else
|
290
|
-
|
344
|
+
headers[SET_COOKIE] = set_cookie_header(key, value)
|
291
345
|
end
|
292
346
|
end
|
293
|
-
module_function :add_cookie_to_header
|
294
347
|
|
295
|
-
|
296
|
-
|
297
|
-
|
348
|
+
# :call-seq:
|
349
|
+
# delete_set_cookie_header(key, value = {}) -> encoded string
|
350
|
+
#
|
351
|
+
# Generate an encoded string based on the given +key+ and +value+ using
|
352
|
+
# set_cookie_header for the purpose of causing the specified cookie to be
|
353
|
+
# deleted. The +value+ may be an instance of +Hash+ and can include
|
354
|
+
# attributes as outlined by set_cookie_header. The encoded cookie will have
|
355
|
+
# a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
|
356
|
+
# +value+. When used with the +set-cookie+ header, it will cause the client
|
357
|
+
# to *remove* any matching cookie.
|
358
|
+
#
|
359
|
+
# delete_set_cookie_header("myname")
|
360
|
+
# # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
361
|
+
#
|
362
|
+
def delete_set_cookie_header(key, value = {})
|
363
|
+
set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
|
298
364
|
end
|
299
|
-
module_function :set_cookie_header!
|
300
365
|
|
301
366
|
def make_delete_cookie_header(header, key, value)
|
302
|
-
|
303
|
-
when nil, ''
|
304
|
-
cookies = []
|
305
|
-
when String
|
306
|
-
cookies = header.split("\n")
|
307
|
-
when Array
|
308
|
-
cookies = header
|
309
|
-
end
|
310
|
-
|
311
|
-
cookies.reject! { |cookie|
|
312
|
-
if value[:domain]
|
313
|
-
cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
|
314
|
-
elsif value[:path]
|
315
|
-
cookie =~ /\A#{escape(key)}=.*path=#{value[:path]}/
|
316
|
-
else
|
317
|
-
cookie =~ /\A#{escape(key)}=/
|
318
|
-
end
|
319
|
-
}
|
367
|
+
warn("make_delete_cookie_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
|
320
368
|
|
321
|
-
|
369
|
+
delete_set_cookie_header!(header, key, value)
|
322
370
|
end
|
323
|
-
module_function :make_delete_cookie_header
|
324
371
|
|
325
|
-
def delete_cookie_header!(
|
326
|
-
|
327
|
-
|
372
|
+
def delete_cookie_header!(headers, key, value = {})
|
373
|
+
headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
|
374
|
+
|
375
|
+
return nil
|
328
376
|
end
|
329
|
-
module_function :delete_cookie_header!
|
330
377
|
|
331
|
-
# Adds a cookie that will *remove* a cookie from the client. Hence the
|
332
|
-
# strange method name.
|
333
378
|
def add_remove_cookie_to_header(header, key, value = {})
|
334
|
-
|
379
|
+
warn("add_remove_cookie_to_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
|
380
|
+
|
381
|
+
delete_set_cookie_header!(header, key, value)
|
382
|
+
end
|
335
383
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
384
|
+
# :call-seq:
|
385
|
+
# delete_set_cookie_header!(header, key, value = {}) -> header value
|
386
|
+
#
|
387
|
+
# Set an expired cookie in the specified headers with the given cookie
|
388
|
+
# +key+ and +value+ using delete_set_cookie_header. This causes
|
389
|
+
# the client to immediately delete the specified cookie.
|
390
|
+
#
|
391
|
+
# delete_set_cookie_header!(nil, "mycookie")
|
392
|
+
# # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
393
|
+
#
|
394
|
+
# If the header is non-nil, it will be modified in place.
|
395
|
+
#
|
396
|
+
# header = []
|
397
|
+
# delete_set_cookie_header!(header, "mycookie")
|
398
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
399
|
+
# header
|
400
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
401
|
+
#
|
402
|
+
def delete_set_cookie_header!(header, key, value = {})
|
403
|
+
if header
|
404
|
+
header = Array(header)
|
405
|
+
header << delete_set_cookie_header(key, value)
|
406
|
+
else
|
407
|
+
header = delete_set_cookie_header(key, value)
|
408
|
+
end
|
340
409
|
|
410
|
+
return header
|
341
411
|
end
|
342
|
-
module_function :add_remove_cookie_to_header
|
343
412
|
|
344
413
|
def rfc2822(time)
|
345
414
|
time.rfc2822
|
346
415
|
end
|
347
|
-
module_function :rfc2822
|
348
|
-
|
349
|
-
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
|
350
|
-
# of '% %b %Y'.
|
351
|
-
# It assumes that the time is in GMT to comply to the RFC 2109.
|
352
|
-
#
|
353
|
-
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
|
354
|
-
# that I'm certain someone implemented only that option.
|
355
|
-
# Do not use %a and %b from Time.strptime, it would use localized names for
|
356
|
-
# weekday and month.
|
357
|
-
#
|
358
|
-
def rfc2109(time)
|
359
|
-
wday = Time::RFC2822_DAY_NAME[time.wday]
|
360
|
-
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
|
361
|
-
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
|
362
|
-
end
|
363
|
-
module_function :rfc2109
|
364
416
|
|
365
417
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
366
418
|
# Returns nil if the header is missing or syntactically invalid.
|
367
419
|
# Returns an empty array if none of the ranges are satisfiable.
|
368
420
|
def byte_ranges(env, size)
|
369
|
-
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
|
370
421
|
get_byte_ranges env['HTTP_RANGE'], size
|
371
422
|
end
|
372
|
-
module_function :byte_ranges
|
373
423
|
|
374
424
|
def get_byte_ranges(http_range, size)
|
375
425
|
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
376
426
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
377
427
|
ranges = []
|
378
428
|
$1.split(/,\s*/).each do |range_spec|
|
379
|
-
return nil
|
380
|
-
|
381
|
-
r0
|
382
|
-
|
383
|
-
return nil if r1.nil?
|
429
|
+
return nil unless range_spec =~ /(\d*)-(\d*)/
|
430
|
+
r0, r1 = $1, $2
|
431
|
+
if r0.empty?
|
432
|
+
return nil if r1.empty?
|
384
433
|
# suffix-byte-range-spec, represents trailing suffix of file
|
385
434
|
r0 = size - r1.to_i
|
386
435
|
r0 = 0 if r0 < 0
|
387
436
|
r1 = size - 1
|
388
437
|
else
|
389
438
|
r0 = r0.to_i
|
390
|
-
if r1.
|
439
|
+
if r1.empty?
|
391
440
|
r1 = size - 1
|
392
441
|
else
|
393
442
|
r1 = r1.to_i
|
394
443
|
return nil if r1 < r0 # backwards range is syntactically invalid
|
395
|
-
r1 = size-1 if r1 >= size
|
444
|
+
r1 = size - 1 if r1 >= size
|
396
445
|
end
|
397
446
|
end
|
398
447
|
ranges << (r0..r1) if r0 <= r1
|
399
448
|
end
|
400
449
|
ranges
|
401
450
|
end
|
402
|
-
module_function :get_byte_ranges
|
403
451
|
|
404
|
-
#
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
452
|
+
# :nocov:
|
453
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
454
|
+
# Constant time string comparison.
|
455
|
+
#
|
456
|
+
# NOTE: the values compared should be of fixed length, such as strings
|
457
|
+
# that have already been processed by HMAC. This should not be used
|
458
|
+
# on variable length plaintext strings because it could leak length info
|
459
|
+
# via timing attacks.
|
460
|
+
def secure_compare(a, b)
|
461
|
+
return false unless a.bytesize == b.bytesize
|
412
462
|
|
413
|
-
|
463
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
464
|
+
end
|
465
|
+
# :nocov:
|
466
|
+
else
|
467
|
+
def secure_compare(a, b)
|
468
|
+
return false unless a.bytesize == b.bytesize
|
414
469
|
|
415
|
-
|
416
|
-
|
417
|
-
|
470
|
+
l = a.unpack("C*")
|
471
|
+
|
472
|
+
r, i = 0, -1
|
473
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
474
|
+
r == 0
|
475
|
+
end
|
418
476
|
end
|
419
|
-
module_function :secure_compare
|
420
477
|
|
421
478
|
# Context allows the use of a compatible middleware at different points
|
422
479
|
# in a request handling stack. A compatible middleware must define
|
@@ -439,98 +496,49 @@ module Rack
|
|
439
496
|
self.class.new(@for, app)
|
440
497
|
end
|
441
498
|
|
442
|
-
def context(env, app
|
499
|
+
def context(env, app = @app)
|
443
500
|
recontext(app).call(env)
|
444
501
|
end
|
445
502
|
end
|
446
503
|
|
447
|
-
# A
|
504
|
+
# A wrapper around Headers
|
448
505
|
# header when set.
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
@names = {}
|
457
|
-
hash.each { |k, v| self[k] = v }
|
458
|
-
end
|
459
|
-
|
460
|
-
# on dup/clone, we need to duplicate @names hash
|
461
|
-
def initialize_copy(other)
|
462
|
-
super
|
463
|
-
@names = other.names.dup
|
464
|
-
end
|
465
|
-
|
466
|
-
def each
|
467
|
-
super do |k, v|
|
468
|
-
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
506
|
+
#
|
507
|
+
# @api private
|
508
|
+
class HeaderHash < Hash # :nodoc:
|
509
|
+
def self.[](headers)
|
510
|
+
warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
|
511
|
+
if headers.is_a?(Headers) && !headers.frozen?
|
512
|
+
return headers
|
469
513
|
end
|
470
|
-
end
|
471
514
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
hash
|
515
|
+
new_headers = Headers.new
|
516
|
+
headers.each{|k,v| new_headers[k] = v}
|
517
|
+
new_headers
|
476
518
|
end
|
477
519
|
|
478
|
-
def
|
479
|
-
|
520
|
+
def self.new(hash = {})
|
521
|
+
warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
|
522
|
+
headers = Headers.new
|
523
|
+
hash.each{|k,v| headers[k] = v}
|
524
|
+
headers
|
480
525
|
end
|
481
526
|
|
482
|
-
def
|
483
|
-
|
484
|
-
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
|
485
|
-
@names[canonical] = k
|
486
|
-
super k, v
|
527
|
+
def self.allocate
|
528
|
+
raise TypeError, "cannot allocate HeaderHash"
|
487
529
|
end
|
488
|
-
|
489
|
-
def delete(k)
|
490
|
-
canonical = k.downcase
|
491
|
-
result = super @names.delete(canonical)
|
492
|
-
result
|
493
|
-
end
|
494
|
-
|
495
|
-
def include?(k)
|
496
|
-
super || @names.include?(k.downcase)
|
497
|
-
end
|
498
|
-
|
499
|
-
alias_method :has_key?, :include?
|
500
|
-
alias_method :member?, :include?
|
501
|
-
alias_method :key?, :include?
|
502
|
-
|
503
|
-
def merge!(other)
|
504
|
-
other.each { |k, v| self[k] = v }
|
505
|
-
self
|
506
|
-
end
|
507
|
-
|
508
|
-
def merge(other)
|
509
|
-
hash = dup
|
510
|
-
hash.merge! other
|
511
|
-
end
|
512
|
-
|
513
|
-
def replace(other)
|
514
|
-
clear
|
515
|
-
other.each { |k, v| self[k] = v }
|
516
|
-
self
|
517
|
-
end
|
518
|
-
|
519
|
-
protected
|
520
|
-
def names
|
521
|
-
@names
|
522
|
-
end
|
523
530
|
end
|
524
531
|
|
525
532
|
# Every standard HTTP code mapped to the appropriate message.
|
526
533
|
# Generated with:
|
527
|
-
#
|
528
|
-
#
|
529
|
-
#
|
534
|
+
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
|
535
|
+
# ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
|
536
|
+
# puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
|
530
537
|
HTTP_STATUS_CODES = {
|
531
538
|
100 => 'Continue',
|
532
539
|
101 => 'Switching Protocols',
|
533
540
|
102 => 'Processing',
|
541
|
+
103 => 'Early Hints',
|
534
542
|
200 => 'OK',
|
535
543
|
201 => 'Created',
|
536
544
|
202 => 'Accepted',
|
@@ -547,6 +555,7 @@ module Rack
|
|
547
555
|
303 => 'See Other',
|
548
556
|
304 => 'Not Modified',
|
549
557
|
305 => 'Use Proxy',
|
558
|
+
306 => '(Unused)',
|
550
559
|
307 => 'Temporary Redirect',
|
551
560
|
308 => 'Permanent Redirect',
|
552
561
|
400 => 'Bad Request',
|
@@ -571,6 +580,7 @@ module Rack
|
|
571
580
|
422 => 'Unprocessable Entity',
|
572
581
|
423 => 'Locked',
|
573
582
|
424 => 'Failed Dependency',
|
583
|
+
425 => 'Too Early',
|
574
584
|
426 => 'Upgrade Required',
|
575
585
|
428 => 'Precondition Required',
|
576
586
|
429 => 'Too Many Requests',
|
@@ -585,12 +595,13 @@ module Rack
|
|
585
595
|
506 => 'Variant Also Negotiates',
|
586
596
|
507 => 'Insufficient Storage',
|
587
597
|
508 => 'Loop Detected',
|
598
|
+
509 => 'Bandwidth Limit Exceeded',
|
588
599
|
510 => 'Not Extended',
|
589
600
|
511 => 'Network Authentication Required'
|
590
601
|
}
|
591
602
|
|
592
603
|
# Responses with HTTP status codes that should not have an entity body
|
593
|
-
STATUS_WITH_NO_ENTITY_BODY =
|
604
|
+
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
594
605
|
|
595
606
|
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
596
607
|
[message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
|
@@ -598,12 +609,11 @@ module Rack
|
|
598
609
|
|
599
610
|
def status_code(status)
|
600
611
|
if status.is_a?(Symbol)
|
601
|
-
SYMBOL_TO_STATUS_CODE
|
612
|
+
SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
|
602
613
|
else
|
603
614
|
status.to_i
|
604
615
|
end
|
605
616
|
end
|
606
|
-
module_function :status_code
|
607
617
|
|
608
618
|
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
|
609
619
|
|
@@ -617,18 +627,16 @@ module Rack
|
|
617
627
|
part == '..' ? clean.pop : clean << part
|
618
628
|
end
|
619
629
|
|
620
|
-
|
621
|
-
|
622
|
-
|
630
|
+
clean_path = clean.join(::File::SEPARATOR)
|
631
|
+
clean_path.prepend("/") if parts.empty? || parts.first.empty?
|
632
|
+
clean_path
|
623
633
|
end
|
624
|
-
module_function :clean_path_info
|
625
634
|
|
626
|
-
NULL_BYTE = "\0"
|
635
|
+
NULL_BYTE = "\0"
|
627
636
|
|
628
637
|
def valid_path?(path)
|
629
638
|
path.valid_encoding? && !path.include?(NULL_BYTE)
|
630
639
|
end
|
631
|
-
module_function :valid_path?
|
632
640
|
|
633
641
|
end
|
634
642
|
end
|