rack 1.0.1 → 2.2.21
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 +7 -0
- data/CHANGELOG.md +829 -0
- data/CONTRIBUTING.md +136 -0
- data/{COPYING → MIT-LICENSE} +4 -2
- data/README.rdoc +355 -0
- data/Rakefile +64 -98
- data/SPEC.rdoc +292 -0
- data/bin/rackup +3 -174
- data/contrib/rack.png +0 -0
- data/contrib/rack.svg +150 -0
- data/contrib/rack_logo.svg +164 -111
- data/contrib/rdoc.css +412 -0
- data/example/lobster.ru +2 -0
- data/example/protectedlobster.rb +4 -2
- data/example/protectedlobster.ru +3 -1
- data/lib/rack/auth/abstract/handler.rb +7 -5
- data/lib/rack/auth/abstract/request.rb +12 -2
- data/lib/rack/auth/basic.rb +6 -4
- data/lib/rack/auth/digest/md5.rb +20 -13
- data/lib/rack/auth/digest/nonce.rb +6 -4
- data/lib/rack/auth/digest/params.rb +11 -12
- data/lib/rack/auth/digest/request.rb +15 -12
- data/lib/rack/body_proxy.rb +45 -0
- data/lib/rack/builder.rb +213 -19
- data/lib/rack/cascade.rb +50 -18
- data/lib/rack/chunked.rb +95 -27
- data/lib/rack/common_logger.rb +84 -0
- data/lib/rack/conditional_get.rb +83 -0
- data/lib/rack/config.rb +22 -0
- data/lib/rack/content_length.rb +19 -10
- data/lib/rack/content_type.rb +12 -5
- data/lib/rack/core_ext/regexp.rb +14 -0
- data/lib/rack/deflater.rb +90 -42
- data/lib/rack/directory.rb +118 -72
- data/lib/rack/etag.rb +77 -0
- data/lib/rack/events.rb +153 -0
- data/lib/rack/file.rb +4 -85
- data/lib/rack/files.rb +218 -0
- data/lib/rack/handler/cgi.rb +22 -24
- data/lib/rack/handler/fastcgi.rb +34 -24
- data/lib/rack/handler/lsws.rb +16 -15
- data/lib/rack/handler/scgi.rb +38 -29
- data/lib/rack/handler/thin.rb +23 -7
- data/lib/rack/handler/webrick.rb +95 -37
- data/lib/rack/handler.rb +49 -14
- data/lib/rack/head.rb +19 -13
- data/lib/rack/lint.rb +466 -217
- data/lib/rack/lobster.rb +19 -14
- data/lib/rack/lock.rb +24 -8
- data/lib/rack/logger.rb +20 -0
- data/lib/rack/media_type.rb +53 -0
- data/lib/rack/method_override.rb +52 -0
- data/lib/rack/mime.rb +652 -171
- data/lib/rack/mock.rb +203 -64
- data/lib/rack/multipart/generator.rb +97 -0
- data/lib/rack/multipart/parser.rb +440 -0
- data/lib/rack/multipart/uploaded_file.rb +41 -0
- data/lib/rack/multipart.rb +64 -0
- data/lib/rack/null_logger.rb +39 -0
- data/lib/rack/query_parser.rb +266 -0
- data/lib/rack/recursive.rb +18 -11
- data/lib/rack/reloader.rb +13 -5
- data/lib/rack/request.rb +607 -191
- data/lib/rack/response.rb +245 -110
- data/lib/rack/rewindable_input.rb +15 -21
- data/lib/rack/runtime.rb +34 -0
- data/lib/rack/sendfile.rb +192 -0
- data/lib/rack/server.rb +466 -0
- data/lib/rack/session/abstract/id.rb +449 -68
- data/lib/rack/session/cookie.rb +159 -47
- data/lib/rack/session/memcache.rb +4 -103
- data/lib/rack/session/pool.rb +39 -49
- data/lib/rack/show_exceptions.rb +390 -0
- data/lib/rack/{showstatus.rb → show_status.rb} +21 -14
- data/lib/rack/static.rb +164 -14
- data/lib/rack/tempfile_reaper.rb +22 -0
- data/lib/rack/urlmap.rb +60 -18
- data/lib/rack/utils.rb +508 -277
- data/lib/rack/version.rb +29 -0
- data/lib/rack.rb +85 -34
- data/rack.gemspec +43 -51
- metadata +119 -207
- data/KNOWN-ISSUES +0 -18
- data/RDOX +0 -428
- data/README +0 -364
- data/SPEC +0 -164
- data/lib/rack/adapter/camping.rb +0 -22
- data/lib/rack/auth/openid.rb +0 -480
- data/lib/rack/commonlogger.rb +0 -61
- data/lib/rack/conditionalget.rb +0 -47
- data/lib/rack/handler/evented_mongrel.rb +0 -8
- data/lib/rack/handler/mongrel.rb +0 -87
- data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
- data/lib/rack/methodoverride.rb +0 -27
- data/lib/rack/showexceptions.rb +0 -349
- data/test/cgi/lighttpd.conf +0 -20
- data/test/cgi/test +0 -9
- data/test/cgi/test.fcgi +0 -8
- data/test/cgi/test.ru +0 -7
- data/test/multipart/binary +0 -0
- data/test/multipart/empty +0 -10
- data/test/multipart/ie +0 -6
- data/test/multipart/nested +0 -10
- data/test/multipart/none +0 -9
- data/test/multipart/semicolon +0 -6
- data/test/multipart/text +0 -10
- data/test/spec_rack_auth_basic.rb +0 -73
- data/test/spec_rack_auth_digest.rb +0 -226
- data/test/spec_rack_auth_openid.rb +0 -84
- data/test/spec_rack_builder.rb +0 -84
- data/test/spec_rack_camping.rb +0 -51
- data/test/spec_rack_cascade.rb +0 -50
- data/test/spec_rack_cgi.rb +0 -89
- data/test/spec_rack_chunked.rb +0 -62
- data/test/spec_rack_commonlogger.rb +0 -32
- data/test/spec_rack_conditionalget.rb +0 -41
- data/test/spec_rack_content_length.rb +0 -43
- data/test/spec_rack_content_type.rb +0 -30
- data/test/spec_rack_deflater.rb +0 -127
- data/test/spec_rack_directory.rb +0 -61
- data/test/spec_rack_fastcgi.rb +0 -89
- data/test/spec_rack_file.rb +0 -75
- data/test/spec_rack_handler.rb +0 -43
- data/test/spec_rack_head.rb +0 -30
- data/test/spec_rack_lint.rb +0 -521
- data/test/spec_rack_lobster.rb +0 -45
- data/test/spec_rack_lock.rb +0 -38
- data/test/spec_rack_methodoverride.rb +0 -60
- data/test/spec_rack_mock.rb +0 -157
- data/test/spec_rack_mongrel.rb +0 -189
- data/test/spec_rack_recursive.rb +0 -77
- data/test/spec_rack_request.rb +0 -504
- data/test/spec_rack_response.rb +0 -218
- data/test/spec_rack_rewindable_input.rb +0 -118
- data/test/spec_rack_session_cookie.rb +0 -82
- data/test/spec_rack_session_memcache.rb +0 -240
- data/test/spec_rack_session_pool.rb +0 -172
- data/test/spec_rack_showexceptions.rb +0 -21
- data/test/spec_rack_showstatus.rb +0 -72
- data/test/spec_rack_static.rb +0 -37
- data/test/spec_rack_thin.rb +0 -91
- data/test/spec_rack_urlmap.rb +0 -185
- data/test/spec_rack_utils.rb +0 -387
- data/test/spec_rack_webrick.rb +0 -130
- data/test/testrequest.rb +0 -57
- 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,166 +1,407 @@
|
|
|
1
1
|
# -*- encoding: binary -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'fileutils'
|
|
3
6
|
require 'set'
|
|
4
7
|
require 'tempfile'
|
|
8
|
+
require 'time'
|
|
9
|
+
|
|
10
|
+
require_relative 'query_parser'
|
|
5
11
|
|
|
6
12
|
module Rack
|
|
7
13
|
# Rack::Utils contains a grab-bag of useful methods for writing web
|
|
8
14
|
# applications adopted from all kinds of Ruby libraries.
|
|
9
15
|
|
|
10
16
|
module Utils
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
(require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
|
|
18
|
+
|
|
19
|
+
ParameterTypeError = QueryParser::ParameterTypeError
|
|
20
|
+
InvalidParameterError = QueryParser::InvalidParameterError
|
|
21
|
+
DEFAULT_SEP = QueryParser::DEFAULT_SEP
|
|
22
|
+
COMMON_SEP = QueryParser::COMMON_SEP
|
|
23
|
+
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
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
attr_accessor :default_query_parser
|
|
31
|
+
end
|
|
32
|
+
# The default number of bytes to allow parameter keys to take up.
|
|
33
|
+
# This helps prevent a rogue client from flooding a Request.
|
|
34
|
+
self.default_query_parser = QueryParser.make_default(65536, 100)
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# URI escapes. (CGI style space to +)
|
|
14
39
|
def escape(s)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}.tr(' ', '+')
|
|
18
|
-
end
|
|
19
|
-
module_function :escape
|
|
20
|
-
|
|
21
|
-
# Unescapes a URI escaped string. (Stolen from Camping).
|
|
22
|
-
def unescape(s)
|
|
23
|
-
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
|
24
|
-
[$1.delete('%')].pack('H*')
|
|
25
|
-
}
|
|
26
|
-
end
|
|
27
|
-
module_function :unescape
|
|
28
|
-
|
|
29
|
-
DEFAULT_SEP = /[&;] */n
|
|
30
|
-
|
|
31
|
-
# Stolen from Mongrel, with some small modifications:
|
|
32
|
-
# Parses a query string by breaking it up at the '&'
|
|
33
|
-
# and ';' characters. You can also use this to parse
|
|
34
|
-
# cookies by changing the characters used in the second
|
|
35
|
-
# parameter (which defaults to '&;').
|
|
36
|
-
def parse_query(qs, d = nil)
|
|
37
|
-
params = {}
|
|
38
|
-
|
|
39
|
-
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
|
40
|
-
k, v = p.split('=', 2).map { |x| unescape(x) }
|
|
41
|
-
|
|
42
|
-
if cur = params[k]
|
|
43
|
-
if cur.class == Array
|
|
44
|
-
params[k] << v
|
|
45
|
-
else
|
|
46
|
-
params[k] = [cur, v]
|
|
47
|
-
end
|
|
48
|
-
else
|
|
49
|
-
params[k] = v
|
|
50
|
-
end
|
|
51
|
-
end
|
|
40
|
+
URI.encode_www_form_component(s)
|
|
41
|
+
end
|
|
52
42
|
|
|
53
|
-
|
|
43
|
+
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
|
|
44
|
+
# true URI escaping.
|
|
45
|
+
def escape_path(s)
|
|
46
|
+
RFC2396_PARSER.escape s
|
|
54
47
|
end
|
|
55
|
-
module_function :parse_query
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
|
|
50
|
+
# unescaping query parameters or form components.
|
|
51
|
+
def unescape_path(s)
|
|
52
|
+
RFC2396_PARSER.unescape s
|
|
53
|
+
end
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
|
|
56
|
+
# target encoding of the string returned, and it defaults to UTF-8
|
|
57
|
+
def unescape(s, encoding = Encoding::UTF_8)
|
|
58
|
+
URI.decode_www_form_component(s, encoding)
|
|
59
|
+
end
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
name
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
61
|
+
class << self
|
|
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=
|
|
70
|
+
end
|
|
71
|
+
|
|
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.
|
|
74
|
+
# Set to `0` for no limit.
|
|
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
|
|
80
|
+
|
|
81
|
+
def self.param_depth_limit
|
|
82
|
+
default_query_parser.param_depth_limit
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.param_depth_limit=(v)
|
|
86
|
+
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
|
|
87
|
+
end
|
|
88
|
+
|
|
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
|
+
if defined?(Process::CLOCK_MONOTONIC)
|
|
98
|
+
def clock_time
|
|
99
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
95
100
|
end
|
|
101
|
+
else
|
|
102
|
+
# :nocov:
|
|
103
|
+
def clock_time
|
|
104
|
+
Time.now.to_f
|
|
105
|
+
end
|
|
106
|
+
# :nocov:
|
|
107
|
+
end
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
def parse_query(qs, d = nil, &unescaper)
|
|
110
|
+
Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def parse_nested_query(qs, d = nil)
|
|
114
|
+
Rack::Utils.default_query_parser.parse_nested_query(qs, d)
|
|
98
115
|
end
|
|
99
|
-
module_function :normalize_params
|
|
100
116
|
|
|
101
117
|
def build_query(params)
|
|
102
118
|
params.map { |k, v|
|
|
103
119
|
if v.class == Array
|
|
104
120
|
build_query(v.map { |x| [k, x] })
|
|
105
121
|
else
|
|
106
|
-
"#{escape(k)}=#{escape(v)}"
|
|
122
|
+
v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
|
|
107
123
|
end
|
|
108
124
|
}.join("&")
|
|
109
125
|
end
|
|
110
|
-
|
|
126
|
+
|
|
127
|
+
def build_nested_query(value, prefix = nil)
|
|
128
|
+
case value
|
|
129
|
+
when Array
|
|
130
|
+
value.map { |v|
|
|
131
|
+
build_nested_query(v, "#{prefix}[]")
|
|
132
|
+
}.join("&")
|
|
133
|
+
when Hash
|
|
134
|
+
value.map { |k, v|
|
|
135
|
+
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
|
|
136
|
+
}.delete_if(&:empty?).join('&')
|
|
137
|
+
when nil
|
|
138
|
+
prefix
|
|
139
|
+
else
|
|
140
|
+
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
|
141
|
+
"#{prefix}=#{escape(value)}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def q_values(q_value_header)
|
|
146
|
+
q_value_header.to_s.split(',').map do |part|
|
|
147
|
+
value, parameters = part.split(';', 2).map(&:strip)
|
|
148
|
+
quality = 1.0
|
|
149
|
+
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
|
|
150
|
+
quality = md[1].to_f
|
|
151
|
+
end
|
|
152
|
+
[value, quality]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Return best accept value to use, based on the algorithm
|
|
157
|
+
# in RFC 2616 Section 14. If there are multiple best
|
|
158
|
+
# matches (same specificity and quality), the value returned
|
|
159
|
+
# is arbitrary.
|
|
160
|
+
def best_q_match(q_value_header, available_mimes)
|
|
161
|
+
values = q_values(q_value_header)
|
|
162
|
+
|
|
163
|
+
matches = values.map do |req_mime, quality|
|
|
164
|
+
match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
|
|
165
|
+
next unless match
|
|
166
|
+
[match, quality]
|
|
167
|
+
end.compact.sort_by do |match, quality|
|
|
168
|
+
(match.split('/', 2).count('*') * -10) + quality
|
|
169
|
+
end.last
|
|
170
|
+
matches && matches.first
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
ESCAPE_HTML = {
|
|
174
|
+
"&" => "&",
|
|
175
|
+
"<" => "<",
|
|
176
|
+
">" => ">",
|
|
177
|
+
"'" => "'",
|
|
178
|
+
'"' => """,
|
|
179
|
+
"/" => "/"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
|
|
111
183
|
|
|
112
184
|
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
|
113
185
|
def escape_html(string)
|
|
114
|
-
string.to_s.gsub(
|
|
115
|
-
gsub("<", "<").
|
|
116
|
-
gsub(">", ">").
|
|
117
|
-
gsub("'", "'").
|
|
118
|
-
gsub('"', """)
|
|
186
|
+
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
|
|
119
187
|
end
|
|
120
|
-
module_function :escape_html
|
|
121
188
|
|
|
122
189
|
def select_best_encoding(available_encodings, accept_encoding)
|
|
123
190
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
|
124
191
|
|
|
125
|
-
expanded_accept_encoding =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
192
|
+
expanded_accept_encoding = []
|
|
193
|
+
|
|
194
|
+
accept_encoding.each do |m, q|
|
|
195
|
+
preference = available_encodings.index(m) || available_encodings.size
|
|
196
|
+
|
|
197
|
+
if m == "*"
|
|
198
|
+
(available_encodings - accept_encoding.map(&:first)).each do |m2|
|
|
199
|
+
expanded_accept_encoding << [m2, q, preference]
|
|
131
200
|
end
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
201
|
+
else
|
|
202
|
+
expanded_accept_encoding << [m, q, preference]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
135
205
|
|
|
136
|
-
encoding_candidates = expanded_accept_encoding
|
|
206
|
+
encoding_candidates = expanded_accept_encoding
|
|
207
|
+
.sort_by { |_, q, p| [-q, p] }
|
|
208
|
+
.map!(&:first)
|
|
137
209
|
|
|
138
210
|
unless encoding_candidates.include?("identity")
|
|
139
211
|
encoding_candidates.push("identity")
|
|
140
212
|
end
|
|
141
213
|
|
|
142
|
-
expanded_accept_encoding.
|
|
143
|
-
q == 0.0
|
|
144
|
-
|
|
145
|
-
encoding_candidates.delete(m)
|
|
146
|
-
}
|
|
214
|
+
expanded_accept_encoding.each do |m, q|
|
|
215
|
+
encoding_candidates.delete(m) if q == 0.0
|
|
216
|
+
end
|
|
147
217
|
|
|
148
|
-
|
|
218
|
+
(encoding_candidates & available_encodings)[0]
|
|
149
219
|
end
|
|
150
|
-
module_function :select_best_encoding
|
|
151
220
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
221
|
+
def parse_cookies(env)
|
|
222
|
+
parse_cookies_header env[HTTP_COOKIE]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def parse_cookies_header(header)
|
|
226
|
+
# According to RFC 6265:
|
|
227
|
+
# The syntax for cookie headers only supports semicolons
|
|
228
|
+
# User Agent -> Server ==
|
|
229
|
+
# Cookie: SID=31d4d96e407aad42; lang=en-US
|
|
230
|
+
return {} unless header
|
|
231
|
+
header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
|
|
232
|
+
next if cookie.empty?
|
|
233
|
+
key, value = cookie.split('=', 2)
|
|
234
|
+
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
|
157
235
|
end
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def add_cookie_to_header(header, key, value)
|
|
239
|
+
case value
|
|
240
|
+
when Hash
|
|
241
|
+
domain = "; domain=#{value[:domain]}" if value[:domain]
|
|
242
|
+
path = "; path=#{value[:path]}" if value[:path]
|
|
243
|
+
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
|
244
|
+
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
|
245
|
+
secure = "; secure" if value[:secure]
|
|
246
|
+
httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
|
247
|
+
same_site =
|
|
248
|
+
case value[:same_site]
|
|
249
|
+
when false, nil
|
|
250
|
+
nil
|
|
251
|
+
when :none, 'None', :None
|
|
252
|
+
'; SameSite=None'
|
|
253
|
+
when :lax, 'Lax', :Lax
|
|
254
|
+
'; SameSite=Lax'
|
|
255
|
+
when true, :strict, 'Strict', :Strict
|
|
256
|
+
'; SameSite=Strict'
|
|
257
|
+
else
|
|
258
|
+
raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
|
|
259
|
+
end
|
|
260
|
+
value = value[:value]
|
|
261
|
+
end
|
|
262
|
+
value = [value] unless Array === value
|
|
263
|
+
|
|
264
|
+
cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
|
|
265
|
+
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
|
|
266
|
+
|
|
267
|
+
case header
|
|
268
|
+
when nil, ''
|
|
269
|
+
cookie
|
|
270
|
+
when String
|
|
271
|
+
[header, cookie].join("\n")
|
|
272
|
+
when Array
|
|
273
|
+
(header + [cookie]).join("\n")
|
|
274
|
+
else
|
|
275
|
+
raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def set_cookie_header!(header, key, value)
|
|
280
|
+
header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
|
|
281
|
+
nil
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def make_delete_cookie_header(header, key, value)
|
|
285
|
+
case header
|
|
286
|
+
when nil, ''
|
|
287
|
+
cookies = []
|
|
288
|
+
when String
|
|
289
|
+
cookies = header.split("\n")
|
|
290
|
+
when Array
|
|
291
|
+
cookies = header
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
key = escape(key)
|
|
295
|
+
domain = value[:domain]
|
|
296
|
+
path = value[:path]
|
|
297
|
+
regexp = if domain
|
|
298
|
+
if path
|
|
299
|
+
/\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
|
|
300
|
+
else
|
|
301
|
+
/\A#{key}=.*domain=#{domain}(?:;|$)/
|
|
302
|
+
end
|
|
303
|
+
elsif path
|
|
304
|
+
/\A#{key}=.*path=#{path}(?:;|$)/
|
|
305
|
+
else
|
|
306
|
+
/\A#{key}=/
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
cookies.reject! { |cookie| regexp.match? cookie }
|
|
310
|
+
|
|
311
|
+
cookies.join("\n")
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def delete_cookie_header!(header, key, value = {})
|
|
315
|
+
header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Adds a cookie that will *remove* a cookie from the client. Hence the
|
|
320
|
+
# strange method name.
|
|
321
|
+
def add_remove_cookie_to_header(header, key, value = {})
|
|
322
|
+
new_header = make_delete_cookie_header(header, key, value)
|
|
323
|
+
|
|
324
|
+
add_cookie_to_header(new_header, key,
|
|
325
|
+
{ value: '', path: nil, domain: nil,
|
|
326
|
+
max_age: '0',
|
|
327
|
+
expires: Time.at(0) }.merge(value))
|
|
328
|
+
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def rfc2822(time)
|
|
332
|
+
time.rfc2822
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
|
|
336
|
+
# of '% %b %Y'.
|
|
337
|
+
# It assumes that the time is in GMT to comply to the RFC 2109.
|
|
338
|
+
#
|
|
339
|
+
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
|
|
340
|
+
# that I'm certain someone implemented only that option.
|
|
341
|
+
# Do not use %a and %b from Time.strptime, it would use localized names for
|
|
342
|
+
# weekday and month.
|
|
343
|
+
#
|
|
344
|
+
def rfc2109(time)
|
|
345
|
+
wday = RFC2822_DAY_NAME[time.wday]
|
|
346
|
+
mon = RFC2822_MONTH_NAME[time.mon - 1]
|
|
347
|
+
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Parses the "Range:" header, if present, into an array of Range objects.
|
|
351
|
+
# Returns nil if the header is missing or syntactically invalid.
|
|
352
|
+
# Returns an empty array if none of the ranges are satisfiable.
|
|
353
|
+
def byte_ranges(env, size)
|
|
354
|
+
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
|
|
355
|
+
get_byte_ranges env['HTTP_RANGE'], size
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def get_byte_ranges(http_range, size)
|
|
359
|
+
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
|
360
|
+
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
|
361
|
+
ranges = []
|
|
362
|
+
$1.split(/,\s*/).each do |range_spec|
|
|
363
|
+
return nil unless range_spec.include?('-')
|
|
364
|
+
range = range_spec.split('-')
|
|
365
|
+
r0, r1 = range[0], range[1]
|
|
366
|
+
if r0.nil? || r0.empty?
|
|
367
|
+
return nil if r1.nil?
|
|
368
|
+
# suffix-byte-range-spec, represents trailing suffix of file
|
|
369
|
+
r0 = size - r1.to_i
|
|
370
|
+
r0 = 0 if r0 < 0
|
|
371
|
+
r1 = size - 1
|
|
372
|
+
else
|
|
373
|
+
r0 = r0.to_i
|
|
374
|
+
if r1.nil?
|
|
375
|
+
r1 = size - 1
|
|
376
|
+
else
|
|
377
|
+
r1 = r1.to_i
|
|
378
|
+
return nil if r1 < r0 # backwards range is syntactically invalid
|
|
379
|
+
r1 = size - 1 if r1 >= size
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
ranges << (r0..r1) if r0 <= r1
|
|
161
383
|
end
|
|
384
|
+
|
|
385
|
+
return [] if ranges.map(&:size).inject(0, :+) > size
|
|
386
|
+
|
|
387
|
+
ranges
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Constant time string comparison.
|
|
391
|
+
#
|
|
392
|
+
# NOTE: the values compared should be of fixed length, such as strings
|
|
393
|
+
# that have already been processed by HMAC. This should not be used
|
|
394
|
+
# on variable length plaintext strings because it could leak length info
|
|
395
|
+
# via timing attacks.
|
|
396
|
+
def secure_compare(a, b)
|
|
397
|
+
return false unless a.bytesize == b.bytesize
|
|
398
|
+
|
|
399
|
+
l = a.unpack("C*")
|
|
400
|
+
|
|
401
|
+
r, i = 0, -1
|
|
402
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
|
403
|
+
r == 0
|
|
162
404
|
end
|
|
163
|
-
module_function :bytesize
|
|
164
405
|
|
|
165
406
|
# Context allows the use of a compatible middleware at different points
|
|
166
407
|
# in a request handling stack. A compatible middleware must define
|
|
@@ -183,46 +424,73 @@ module Rack
|
|
|
183
424
|
self.class.new(@for, app)
|
|
184
425
|
end
|
|
185
426
|
|
|
186
|
-
def context(env, app
|
|
427
|
+
def context(env, app = @app)
|
|
187
428
|
recontext(app).call(env)
|
|
188
429
|
end
|
|
189
430
|
end
|
|
190
431
|
|
|
191
432
|
# A case-insensitive Hash that preserves the original case of a
|
|
192
433
|
# header when set.
|
|
193
|
-
|
|
194
|
-
|
|
434
|
+
#
|
|
435
|
+
# @api private
|
|
436
|
+
class HeaderHash < Hash # :nodoc:
|
|
437
|
+
def self.[](headers)
|
|
438
|
+
if headers.is_a?(HeaderHash) && !headers.frozen?
|
|
439
|
+
return headers
|
|
440
|
+
else
|
|
441
|
+
return self.new(headers)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def initialize(hash = {})
|
|
446
|
+
super()
|
|
195
447
|
@names = {}
|
|
196
448
|
hash.each { |k, v| self[k] = v }
|
|
197
449
|
end
|
|
198
450
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
451
|
+
# on dup/clone, we need to duplicate @names hash
|
|
452
|
+
def initialize_copy(other)
|
|
453
|
+
super
|
|
454
|
+
@names = other.names.dup
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# on clear, we need to clear @names hash
|
|
458
|
+
def clear
|
|
459
|
+
super
|
|
460
|
+
@names.clear
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def each
|
|
464
|
+
super do |k, v|
|
|
465
|
+
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
|
207
466
|
end
|
|
208
467
|
end
|
|
209
468
|
|
|
469
|
+
def to_hash
|
|
470
|
+
hash = {}
|
|
471
|
+
each { |k, v| hash[k] = v }
|
|
472
|
+
hash
|
|
473
|
+
end
|
|
474
|
+
|
|
210
475
|
def [](k)
|
|
211
|
-
super @names[k.downcase]
|
|
476
|
+
super(k) || super(@names[k.downcase])
|
|
212
477
|
end
|
|
213
478
|
|
|
214
479
|
def []=(k, v)
|
|
215
|
-
|
|
216
|
-
@names[k.
|
|
480
|
+
canonical = k.downcase.freeze
|
|
481
|
+
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
|
|
482
|
+
@names[canonical] = k
|
|
217
483
|
super k, v
|
|
218
484
|
end
|
|
219
485
|
|
|
220
486
|
def delete(k)
|
|
221
|
-
|
|
487
|
+
canonical = k.downcase
|
|
488
|
+
result = super @names.delete(canonical)
|
|
489
|
+
result
|
|
222
490
|
end
|
|
223
491
|
|
|
224
492
|
def include?(k)
|
|
225
|
-
@names.
|
|
493
|
+
super || @names.include?(k.downcase)
|
|
226
494
|
end
|
|
227
495
|
|
|
228
496
|
alias_method :has_key?, :include?
|
|
@@ -238,164 +506,127 @@ module Rack
|
|
|
238
506
|
hash = dup
|
|
239
507
|
hash.merge! other
|
|
240
508
|
end
|
|
509
|
+
|
|
510
|
+
def replace(other)
|
|
511
|
+
clear
|
|
512
|
+
other.each { |k, v| self[k] = v }
|
|
513
|
+
self
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
protected
|
|
517
|
+
def names
|
|
518
|
+
@names
|
|
519
|
+
end
|
|
241
520
|
end
|
|
242
521
|
|
|
243
522
|
# Every standard HTTP code mapped to the appropriate message.
|
|
244
|
-
#
|
|
523
|
+
# Generated with:
|
|
524
|
+
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
|
|
525
|
+
# ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
|
|
526
|
+
# puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
|
|
245
527
|
HTTP_STATUS_CODES = {
|
|
246
|
-
100
|
|
247
|
-
101
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
528
|
+
100 => 'Continue',
|
|
529
|
+
101 => 'Switching Protocols',
|
|
530
|
+
102 => 'Processing',
|
|
531
|
+
103 => 'Early Hints',
|
|
532
|
+
200 => 'OK',
|
|
533
|
+
201 => 'Created',
|
|
534
|
+
202 => 'Accepted',
|
|
535
|
+
203 => 'Non-Authoritative Information',
|
|
536
|
+
204 => 'No Content',
|
|
537
|
+
205 => 'Reset Content',
|
|
538
|
+
206 => 'Partial Content',
|
|
539
|
+
207 => 'Multi-Status',
|
|
540
|
+
208 => 'Already Reported',
|
|
541
|
+
226 => 'IM Used',
|
|
542
|
+
300 => 'Multiple Choices',
|
|
543
|
+
301 => 'Moved Permanently',
|
|
544
|
+
302 => 'Found',
|
|
545
|
+
303 => 'See Other',
|
|
546
|
+
304 => 'Not Modified',
|
|
547
|
+
305 => 'Use Proxy',
|
|
548
|
+
306 => '(Unused)',
|
|
549
|
+
307 => 'Temporary Redirect',
|
|
550
|
+
308 => 'Permanent Redirect',
|
|
551
|
+
400 => 'Bad Request',
|
|
552
|
+
401 => 'Unauthorized',
|
|
553
|
+
402 => 'Payment Required',
|
|
554
|
+
403 => 'Forbidden',
|
|
555
|
+
404 => 'Not Found',
|
|
556
|
+
405 => 'Method Not Allowed',
|
|
557
|
+
406 => 'Not Acceptable',
|
|
558
|
+
407 => 'Proxy Authentication Required',
|
|
559
|
+
408 => 'Request Timeout',
|
|
560
|
+
409 => 'Conflict',
|
|
561
|
+
410 => 'Gone',
|
|
562
|
+
411 => 'Length Required',
|
|
563
|
+
412 => 'Precondition Failed',
|
|
564
|
+
413 => 'Payload Too Large',
|
|
565
|
+
414 => 'URI Too Long',
|
|
566
|
+
415 => 'Unsupported Media Type',
|
|
567
|
+
416 => 'Range Not Satisfiable',
|
|
568
|
+
417 => 'Expectation Failed',
|
|
569
|
+
421 => 'Misdirected Request',
|
|
570
|
+
422 => 'Unprocessable Entity',
|
|
571
|
+
423 => 'Locked',
|
|
572
|
+
424 => 'Failed Dependency',
|
|
573
|
+
425 => 'Too Early',
|
|
574
|
+
426 => 'Upgrade Required',
|
|
575
|
+
428 => 'Precondition Required',
|
|
576
|
+
429 => 'Too Many Requests',
|
|
577
|
+
431 => 'Request Header Fields Too Large',
|
|
578
|
+
451 => 'Unavailable for Legal Reasons',
|
|
579
|
+
500 => 'Internal Server Error',
|
|
580
|
+
501 => 'Not Implemented',
|
|
581
|
+
502 => 'Bad Gateway',
|
|
582
|
+
503 => 'Service Unavailable',
|
|
583
|
+
504 => 'Gateway Timeout',
|
|
584
|
+
505 => 'HTTP Version Not Supported',
|
|
585
|
+
506 => 'Variant Also Negotiates',
|
|
586
|
+
507 => 'Insufficient Storage',
|
|
587
|
+
508 => 'Loop Detected',
|
|
588
|
+
509 => 'Bandwidth Limit Exceeded',
|
|
589
|
+
510 => 'Not Extended',
|
|
590
|
+
511 => 'Network Authentication Required'
|
|
286
591
|
}
|
|
287
592
|
|
|
288
593
|
# Responses with HTTP status codes that should not have an entity body
|
|
289
|
-
STATUS_WITH_NO_ENTITY_BODY =
|
|
594
|
+
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
|
290
595
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
596
|
+
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
|
597
|
+
[message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
|
|
598
|
+
}.flatten]
|
|
294
599
|
|
|
295
|
-
|
|
296
|
-
|
|
600
|
+
def status_code(status)
|
|
601
|
+
if status.is_a?(Symbol)
|
|
602
|
+
SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
|
|
603
|
+
else
|
|
604
|
+
status.to_i
|
|
605
|
+
end
|
|
606
|
+
end
|
|
297
607
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
input = env['rack.input']
|
|
309
|
-
input.rewind
|
|
310
|
-
|
|
311
|
-
boundary_size = Utils.bytesize(boundary) + EOL.size
|
|
312
|
-
bufsize = 16384
|
|
313
|
-
|
|
314
|
-
content_length -= boundary_size
|
|
315
|
-
|
|
316
|
-
read_buffer = ''
|
|
317
|
-
|
|
318
|
-
status = input.read(boundary_size, read_buffer)
|
|
319
|
-
raise EOFError, "bad content body" unless status == boundary + EOL
|
|
320
|
-
|
|
321
|
-
rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
|
|
322
|
-
|
|
323
|
-
loop {
|
|
324
|
-
head = nil
|
|
325
|
-
body = ''
|
|
326
|
-
filename = content_type = name = nil
|
|
327
|
-
|
|
328
|
-
until head && buf =~ rx
|
|
329
|
-
if !head && i = buf.index(EOL+EOL)
|
|
330
|
-
head = buf.slice!(0, i+2) # First \r\n
|
|
331
|
-
buf.slice!(0, 2) # Second \r\n
|
|
332
|
-
|
|
333
|
-
filename = head[/Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;\s]*))/ni, 1]
|
|
334
|
-
content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
|
|
335
|
-
name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
|
|
336
|
-
|
|
337
|
-
if content_type || filename
|
|
338
|
-
body = Tempfile.new("RackMultipart")
|
|
339
|
-
body.binmode if body.respond_to?(:binmode)
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
next
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
# Save the read body part.
|
|
346
|
-
if head && (boundary_size+4 < buf.size)
|
|
347
|
-
body << buf.slice!(0, buf.size - (boundary_size+4))
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
|
|
351
|
-
raise EOFError, "bad content body" if c.nil? || c.empty?
|
|
352
|
-
buf << c
|
|
353
|
-
content_length -= c.size
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
# Save the rest.
|
|
357
|
-
if i = buf.index(rx)
|
|
358
|
-
body << buf.slice!(0, i)
|
|
359
|
-
buf.slice!(0, boundary_size+2)
|
|
360
|
-
|
|
361
|
-
content_length = -1 if $1 == "--"
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
if filename == ""
|
|
365
|
-
# filename is blank which means no file has been selected
|
|
366
|
-
data = nil
|
|
367
|
-
elsif filename
|
|
368
|
-
body.rewind
|
|
369
|
-
|
|
370
|
-
# Take the basename of the upload's original filename.
|
|
371
|
-
# This handles the full Windows paths given by Internet Explorer
|
|
372
|
-
# (and perhaps other broken user agents) without affecting
|
|
373
|
-
# those which give the lone filename.
|
|
374
|
-
filename =~ /^(?:.*[:\\\/])?(.*)/m
|
|
375
|
-
filename = $1
|
|
376
|
-
|
|
377
|
-
data = {:filename => filename, :type => content_type,
|
|
378
|
-
:name => name, :tempfile => body, :head => head}
|
|
379
|
-
elsif !filename && content_type
|
|
380
|
-
body.rewind
|
|
381
|
-
|
|
382
|
-
# Generic multipart cases, not coming from a form
|
|
383
|
-
data = {:type => content_type,
|
|
384
|
-
:name => name, :tempfile => body, :head => head}
|
|
385
|
-
else
|
|
386
|
-
data = body
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
Utils.normalize_params(params, name, data) unless data.nil?
|
|
390
|
-
|
|
391
|
-
break if buf.empty? || content_length == -1
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
input.rewind
|
|
395
|
-
|
|
396
|
-
params
|
|
397
|
-
end
|
|
608
|
+
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
|
|
609
|
+
|
|
610
|
+
def clean_path_info(path_info)
|
|
611
|
+
parts = path_info.split PATH_SEPS
|
|
612
|
+
|
|
613
|
+
clean = []
|
|
614
|
+
|
|
615
|
+
parts.each do |part|
|
|
616
|
+
next if part.empty? || part == '.'
|
|
617
|
+
part == '..' ? clean.pop : clean << part
|
|
398
618
|
end
|
|
619
|
+
|
|
620
|
+
clean_path = clean.join(::File::SEPARATOR)
|
|
621
|
+
clean_path.prepend("/") if parts.empty? || parts.first.empty?
|
|
622
|
+
clean_path
|
|
399
623
|
end
|
|
624
|
+
|
|
625
|
+
NULL_BYTE = "\0"
|
|
626
|
+
|
|
627
|
+
def valid_path?(path)
|
|
628
|
+
path.valid_encoding? && !path.include?(NULL_BYTE)
|
|
629
|
+
end
|
|
630
|
+
|
|
400
631
|
end
|
|
401
632
|
end
|