rack 2.1.4.4 → 2.2.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.

Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +598 -15
  3. data/CONTRIBUTING.md +136 -0
  4. data/README.rdoc +84 -54
  5. data/Rakefile +14 -7
  6. data/{SPEC → SPEC.rdoc} +35 -6
  7. data/lib/rack/auth/abstract/request.rb +0 -2
  8. data/lib/rack/auth/basic.rb +3 -3
  9. data/lib/rack/auth/digest/md5.rb +4 -4
  10. data/lib/rack/auth/digest/request.rb +3 -3
  11. data/lib/rack/body_proxy.rb +13 -9
  12. data/lib/rack/builder.rb +77 -8
  13. data/lib/rack/cascade.rb +23 -8
  14. data/lib/rack/chunked.rb +48 -23
  15. data/lib/rack/common_logger.rb +25 -21
  16. data/lib/rack/conditional_get.rb +18 -16
  17. data/lib/rack/content_length.rb +6 -7
  18. data/lib/rack/content_type.rb +3 -4
  19. data/lib/rack/deflater.rb +45 -35
  20. data/lib/rack/directory.rb +77 -60
  21. data/lib/rack/etag.rb +2 -3
  22. data/lib/rack/events.rb +15 -18
  23. data/lib/rack/file.rb +1 -1
  24. data/lib/rack/files.rb +96 -56
  25. data/lib/rack/handler/cgi.rb +1 -4
  26. data/lib/rack/handler/fastcgi.rb +1 -3
  27. data/lib/rack/handler/lsws.rb +1 -3
  28. data/lib/rack/handler/scgi.rb +1 -3
  29. data/lib/rack/handler/thin.rb +15 -11
  30. data/lib/rack/handler/webrick.rb +12 -5
  31. data/lib/rack/head.rb +0 -2
  32. data/lib/rack/lint.rb +58 -15
  33. data/lib/rack/lobster.rb +3 -5
  34. data/lib/rack/lock.rb +0 -1
  35. data/lib/rack/mock.rb +22 -4
  36. data/lib/rack/multipart/generator.rb +11 -6
  37. data/lib/rack/multipart/parser.rb +12 -32
  38. data/lib/rack/multipart/uploaded_file.rb +13 -7
  39. data/lib/rack/multipart.rb +5 -4
  40. data/lib/rack/query_parser.rb +7 -8
  41. data/lib/rack/recursive.rb +1 -1
  42. data/lib/rack/reloader.rb +1 -3
  43. data/lib/rack/request.rb +172 -76
  44. data/lib/rack/response.rb +62 -19
  45. data/lib/rack/rewindable_input.rb +0 -1
  46. data/lib/rack/runtime.rb +3 -3
  47. data/lib/rack/sendfile.rb +0 -3
  48. data/lib/rack/server.rb +9 -8
  49. data/lib/rack/session/abstract/id.rb +20 -18
  50. data/lib/rack/session/cookie.rb +2 -3
  51. data/lib/rack/session/pool.rb +1 -1
  52. data/lib/rack/show_exceptions.rb +2 -4
  53. data/lib/rack/show_status.rb +1 -3
  54. data/lib/rack/static.rb +13 -6
  55. data/lib/rack/tempfile_reaper.rb +0 -2
  56. data/lib/rack/urlmap.rb +1 -4
  57. data/lib/rack/utils.rb +70 -82
  58. data/lib/rack/version.rb +29 -0
  59. data/lib/rack.rb +7 -16
  60. data/rack.gemspec +31 -29
  61. metadata +14 -15
@@ -17,9 +17,13 @@ module Rack
17
17
 
18
18
  flattened_params.map do |name, file|
19
19
  if file.respond_to?(:original_filename)
20
- ::File.open(file.path, 'rb') do |f|
21
- f.set_encoding(Encoding::BINARY)
22
- content_for_tempfile(f, file, name)
20
+ if file.path
21
+ ::File.open(file.path, 'rb') do |f|
22
+ f.set_encoding(Encoding::BINARY)
23
+ content_for_tempfile(f, file, name)
24
+ end
25
+ else
26
+ content_for_tempfile(file, file, name)
23
27
  end
24
28
  else
25
29
  content_for_other(file, name)
@@ -69,12 +73,13 @@ module Rack
69
73
  end
70
74
 
71
75
  def content_for_tempfile(io, file, name)
76
+ length = ::File.stat(file.path).size if file.path
77
+ filename = "; filename=\"#{Utils.escape(file.original_filename)}\"" if file.original_filename
72
78
  <<-EOF
73
79
  --#{MULTIPART_BOUNDARY}\r
74
- Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
80
+ Content-Disposition: form-data; name="#{name}"#{filename}\r
75
81
  Content-Type: #{file.content_type}\r
76
- Content-Length: #{::File.stat(file.path).size}\r
77
- \r
82
+ #{"Content-Length: #{length}\r\n" if length}\r
78
83
  #{io.read}\r
79
84
  EOF
80
85
  end
@@ -1,16 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/utils'
4
3
  require 'strscan'
5
- require 'rack/core_ext/regexp'
6
4
 
7
5
  module Rack
8
6
  module Multipart
9
7
  class MultipartPartLimitError < Errno::EMFILE; end
10
- class MultipartTotalPartLimitError < StandardError; end
11
8
 
12
9
  class Parser
13
- using ::Rack::RegexpExtensions
10
+ (require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
14
11
 
15
12
  BUFSIZE = 1_048_576
16
13
  TEXT_PLAIN = "text/plain"
@@ -102,12 +99,6 @@ module Rack
102
99
 
103
100
  data = { filename: fn, type: content_type,
104
101
  name: name, tempfile: body, head: head }
105
- elsif !filename && content_type && body.is_a?(IO)
106
- body.rewind
107
-
108
- # Generic multipart cases, not coming from a form
109
- data = { type: content_type,
110
- name: name, tempfile: body, head: head }
111
102
  end
112
103
 
113
104
  yield data
@@ -126,7 +117,7 @@ module Rack
126
117
 
127
118
  include Enumerable
128
119
 
129
- def initialize tempfile
120
+ def initialize(tempfile)
130
121
  @tempfile = tempfile
131
122
  @mime_parts = []
132
123
  @open_files = 0
@@ -136,7 +127,7 @@ module Rack
136
127
  @mime_parts.each { |part| yield part }
137
128
  end
138
129
 
139
- def on_mime_head mime_index, head, filename, content_type, name
130
+ def on_mime_head(mime_index, head, filename, content_type, name)
140
131
  if filename
141
132
  body = @tempfile.call(filename, content_type)
142
133
  body.binmode if body.respond_to?(:binmode)
@@ -149,35 +140,25 @@ module Rack
149
140
 
150
141
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
151
142
 
152
- check_part_limits
143
+ check_open_files
153
144
  end
154
145
 
155
- def on_mime_body mime_index, content
146
+ def on_mime_body(mime_index, content)
156
147
  @mime_parts[mime_index].body << content
157
148
  end
158
149
 
159
- def on_mime_finish mime_index
150
+ def on_mime_finish(mime_index)
160
151
  end
161
152
 
162
153
  private
163
154
 
164
- def check_part_limits
165
- file_limit = Utils.multipart_file_limit
166
- part_limit = Utils.multipart_total_part_limit
167
-
168
- if file_limit && file_limit > 0
169
- if @open_files >= file_limit
155
+ def check_open_files
156
+ if Utils.multipart_part_limit > 0
157
+ if @open_files >= Utils.multipart_part_limit
170
158
  @mime_parts.each(&:close)
171
159
  raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
172
160
  end
173
161
  end
174
-
175
- if part_limit && part_limit > 0
176
- if @mime_parts.size >= part_limit
177
- @mime_parts.each(&:close)
178
- raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
179
- end
180
- end
181
162
  end
182
163
  end
183
164
 
@@ -201,7 +182,7 @@ module Rack
201
182
  @head_regex = /(.*?#{EOL})#{EOL}/m
202
183
  end
203
184
 
204
- def on_read content
185
+ def on_read(content)
205
186
  handle_empty_content!(content)
206
187
  @sbuf.concat content
207
188
  run_parser
@@ -320,9 +301,8 @@ module Rack
320
301
  elsif filename = params['filename*']
321
302
  encoding, _, filename = filename.split("'", 3)
322
303
  end
323
- when BROKEN
304
+ when BROKEN_QUOTED, BROKEN_UNQUOTED
324
305
  filename = $1
325
- filename = $1 if filename =~ /^"(.*)"$/
326
306
  end
327
307
 
328
308
  return unless filename
@@ -359,7 +339,7 @@ module Rack
359
339
  type_subtype = list.first
360
340
  type_subtype.strip!
361
341
  if TEXT_PLAIN == type_subtype
362
- rest = list.drop 1
342
+ rest = list.drop 1
363
343
  rest.each do |param|
364
344
  k, v = param.split('=', 2)
365
345
  k.strip!
@@ -9,17 +9,23 @@ module Rack
9
9
  # The content type of the "uploaded" file
10
10
  attr_accessor :content_type
11
11
 
12
- def initialize(path, content_type = "text/plain", binary = false)
13
- raise "#{path} file does not exist" unless ::File.exist?(path)
12
+ def initialize(filepath = nil, ct = "text/plain", bin = false,
13
+ path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
14
+ if io
15
+ @tempfile = io
16
+ @original_filename = filename
17
+ else
18
+ raise "#{path} file does not exist" unless ::File.exist?(path)
19
+ @original_filename = filename || ::File.basename(path)
20
+ @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
21
+ @tempfile.binmode if binary
22
+ FileUtils.copy_file(path, @tempfile.path)
23
+ end
14
24
  @content_type = content_type
15
- @original_filename = ::File.basename(path)
16
- @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
17
- @tempfile.binmode if binary
18
- FileUtils.copy_file(path, @tempfile.path)
19
25
  end
20
26
 
21
27
  def path
22
- @tempfile.path
28
+ @tempfile.path if @tempfile.respond_to?(:path)
23
29
  end
24
30
  alias_method :local_path, :path
25
31
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/multipart/parser'
3
+ require_relative 'multipart/parser'
4
4
 
5
5
  module Rack
6
6
  # A multipart form data parser, adapted from IOWA.
@@ -16,12 +16,13 @@ module Rack
16
16
  TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
17
17
  CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
18
18
  VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
19
- BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
19
+ BROKEN_QUOTED = /^#{CONDISP}.*;\s*filename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
20
+ BROKEN_UNQUOTED = /^#{CONDISP}.*;\s*filename=(#{TOKEN})/i
20
21
  MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
21
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
22
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
22
23
  MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
23
24
  # Updated definitions from RFC 2231
24
- ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
25
+ ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
25
26
  ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
26
27
  SECTION = /\*[0-9]+/
27
28
  REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'core_ext/regexp'
4
-
5
3
  module Rack
6
4
  class QueryParser
7
- using ::Rack::RegexpExtensions
5
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
8
6
 
9
7
  DEFAULT_SEP = /[&;] */n
10
8
  COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
@@ -64,18 +62,19 @@ module Rack
64
62
  # ParameterTypeError is raised. Users are encouraged to return a 400 in this
65
63
  # case.
66
64
  def parse_nested_query(qs, d = nil)
67
- return {} if qs.nil? || qs.empty?
68
65
  params = make_params
69
66
 
70
- qs.split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
71
- k, v = p.split('=', 2).map! { |s| unescape(s) }
67
+ unless qs.nil? || qs.empty?
68
+ (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
69
+ k, v = p.split('=', 2).map! { |s| unescape(s) }
72
70
 
73
- normalize_params(params, k, v, param_depth_limit)
71
+ normalize_params(params, k, v, param_depth_limit)
72
+ end
74
73
  end
75
74
 
76
75
  return params.to_h
77
76
  rescue ArgumentError => e
78
- raise InvalidParameterError, e.message
77
+ raise InvalidParameterError, e.message, e.backtrace
79
78
  end
80
79
 
81
80
  # normalize_params recursively expands parameters into structural types. If
@@ -19,7 +19,7 @@ module Rack
19
19
  @env[PATH_INFO] = @url.path
20
20
  @env[QUERY_STRING] = @url.query if @url.query
21
21
  @env[HTTP_HOST] = @url.host if @url.host
22
- @env["HTTP_PORT"] = @url.port if @url.port
22
+ @env[HTTP_PORT] = @url.port if @url.port
23
23
  @env[RACK_URL_SCHEME] = @url.scheme if @url.scheme
24
24
 
25
25
  super "forwarding to #{url}"
data/lib/rack/reloader.rb CHANGED
@@ -6,8 +6,6 @@
6
6
 
7
7
  require 'pathname'
8
8
 
9
- require_relative 'core_ext/regexp'
10
-
11
9
  module Rack
12
10
 
13
11
  # High performant source reloader
@@ -24,7 +22,7 @@ module Rack
24
22
  # It is performing a check/reload cycle at the start of every request, but
25
23
  # also respects a cool down time, during which nothing will be done.
26
24
  class Reloader
27
- using ::Rack::RegexpExtensions
25
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
28
26
 
29
27
  def initialize(app, cooldown = 10, backend = Stat)
30
28
  @app = app
data/lib/rack/request.rb CHANGED
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/utils'
4
- require 'rack/media_type'
5
-
6
- require_relative 'core_ext/regexp'
7
-
8
3
  module Rack
9
4
  # Rack::Request provides a convenient interface to a Rack
10
5
  # environment. It is stateless, the environment +env+ passed to the
@@ -15,7 +10,7 @@ module Rack
15
10
  # req.params["data"]
16
11
 
17
12
  class Request
18
- using ::Rack::RegexpExtensions
13
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
19
14
 
20
15
  class << self
21
16
  attr_accessor :ip_filter
@@ -93,7 +88,7 @@ module Rack
93
88
  # assert_equal 'image/png,*/*', request.get_header('Accept')
94
89
  #
95
90
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
96
- def add_header key, v
91
+ def add_header(key, v)
97
92
  if v.nil?
98
93
  get_header key
99
94
  elsif has_header? key
@@ -134,11 +129,23 @@ module Rack
134
129
  # to include the port in a generated URI.
135
130
  DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
136
131
 
132
+ # The address of the client which connected to the proxy.
133
+ HTTP_X_FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR'
134
+
135
+ # The contents of the host/:authority header sent to the proxy.
136
+ HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
137
+
138
+ # The value of the scheme sent to the proxy.
137
139
  HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
138
- HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
139
- HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
140
- HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
141
- HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
140
+
141
+ # The protocol used to connect to the proxy.
142
+ HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
143
+
144
+ # The port used to connect to the proxy.
145
+ HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
146
+
147
+ # Another way for specifing https scheme was used.
148
+ HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
142
149
 
143
150
  def body; get_header(RACK_INPUT) end
144
151
  def script_name; get_header(SCRIPT_NAME).to_s end
@@ -212,19 +219,52 @@ module Rack
212
219
  end
213
220
  end
214
221
 
222
+ # The authority of the incoming request as defined by RFC3976.
223
+ # https://tools.ietf.org/html/rfc3986#section-3.2
224
+ #
225
+ # In HTTP/1, this is the `host` header.
226
+ # In HTTP/2, this is the `:authority` pseudo-header.
215
227
  def authority
216
- get_header(SERVER_NAME) + ':' + get_header(SERVER_PORT)
228
+ forwarded_authority || host_authority || server_authority
229
+ end
230
+
231
+ # The authority as defined by the `SERVER_NAME` and `SERVER_PORT`
232
+ # variables.
233
+ def server_authority
234
+ host = self.server_name
235
+ port = self.server_port
236
+
237
+ if host
238
+ if port
239
+ "#{host}:#{port}"
240
+ else
241
+ host
242
+ end
243
+ end
244
+ end
245
+
246
+ def server_name
247
+ get_header(SERVER_NAME)
248
+ end
249
+
250
+ def server_port
251
+ if port = get_header(SERVER_PORT)
252
+ Integer(port)
253
+ end
217
254
  end
218
255
 
219
256
  def cookies
220
- hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
221
- set_header(k, {})
257
+ hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |key|
258
+ set_header(key, {})
259
+ end
260
+
261
+ string = get_header(HTTP_COOKIE)
262
+
263
+ unless string == get_header(RACK_REQUEST_COOKIE_STRING)
264
+ hash.replace Utils.parse_cookies_header(string)
265
+ set_header(RACK_REQUEST_COOKIE_STRING, string)
222
266
  end
223
- string = get_header HTTP_COOKIE
224
267
 
225
- return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
226
- hash.replace Utils.parse_cookies_header string
227
- set_header(RACK_REQUEST_COOKIE_STRING, string)
228
268
  hash
229
269
  end
230
270
 
@@ -237,52 +277,91 @@ module Rack
237
277
  get_header("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
238
278
  end
239
279
 
240
- def host_with_port
241
- if forwarded = get_header(HTTP_X_FORWARDED_HOST)
242
- forwarded.split(/,\s?/).last
280
+ # The `HTTP_HOST` header.
281
+ def host_authority
282
+ get_header(HTTP_HOST)
283
+ end
284
+
285
+ def host_with_port(authority = self.authority)
286
+ host, _, port = split_authority(authority)
287
+
288
+ if port == DEFAULT_PORTS[self.scheme]
289
+ host
243
290
  else
244
- get_header(HTTP_HOST) || "#{get_header(SERVER_NAME) || get_header(SERVER_ADDR)}:#{get_header(SERVER_PORT)}"
291
+ authority
245
292
  end
246
293
  end
247
294
 
295
+ # Returns a formatted host, suitable for being used in a URI.
248
296
  def host
249
- # Remove port number.
250
- h = host_with_port
251
- if colon_index = h.index(":")
252
- h[0, colon_index]
253
- else
254
- h
255
- end
297
+ split_authority(self.authority)[0]
298
+ end
299
+
300
+ # Returns an address suitable for being to resolve to an address.
301
+ # In the case of a domain name or IPv4 address, the result is the same
302
+ # as +host+. In the case of IPv6 or future address formats, the square
303
+ # brackets are removed.
304
+ def hostname
305
+ split_authority(self.authority)[1]
256
306
  end
257
307
 
258
308
  def port
259
- if port = extract_port(host_with_port)
260
- port.to_i
261
- elsif port = get_header(HTTP_X_FORWARDED_PORT)
262
- port.to_i
263
- elsif has_header?(HTTP_X_FORWARDED_HOST)
264
- DEFAULT_PORTS[scheme]
265
- elsif has_header?(HTTP_X_FORWARDED_PROTO)
266
- DEFAULT_PORTS[extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO))]
267
- else
268
- get_header(SERVER_PORT).to_i
309
+ if authority = self.authority
310
+ _, _, port = split_authority(self.authority)
311
+
312
+ if port
313
+ return port
314
+ end
315
+ end
316
+
317
+ if forwarded_port = self.forwarded_port
318
+ return forwarded_port.first
319
+ end
320
+
321
+ if scheme = self.scheme
322
+ if port = DEFAULT_PORTS[self.scheme]
323
+ return port
324
+ end
325
+ end
326
+
327
+ self.server_port
328
+ end
329
+
330
+ def forwarded_for
331
+ if value = get_header(HTTP_X_FORWARDED_FOR)
332
+ split_header(value).map do |authority|
333
+ split_authority(wrap_ipv6(authority))[1]
334
+ end
335
+ end
336
+ end
337
+
338
+ def forwarded_port
339
+ if value = get_header(HTTP_X_FORWARDED_PORT)
340
+ split_header(value).map(&:to_i)
341
+ end
342
+ end
343
+
344
+ def forwarded_authority
345
+ if value = get_header(HTTP_X_FORWARDED_HOST)
346
+ wrap_ipv6(split_header(value).first)
269
347
  end
270
348
  end
271
349
 
272
350
  def ssl?
273
- scheme == 'https'
351
+ scheme == 'https' || scheme == 'wss'
274
352
  end
275
353
 
276
354
  def ip
277
- remote_addrs = split_ip_addresses(get_header('REMOTE_ADDR'))
355
+ remote_addrs = split_header(get_header('REMOTE_ADDR'))
278
356
  remote_addrs = reject_trusted_ip_addresses(remote_addrs)
279
357
 
280
- return remote_addrs.first if remote_addrs.any?
281
-
282
- forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))
283
- .map { |ip| strip_port(ip) }
358
+ if remote_addrs.any?
359
+ remote_addrs.first
360
+ else
361
+ forwarded_ips = self.forwarded_for
284
362
 
285
- return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
363
+ reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
364
+ end
286
365
  end
287
366
 
288
367
  # The media type (type/subtype) portion of the CONTENT_TYPE header
@@ -323,6 +402,7 @@ module Rack
323
402
  def form_data?
324
403
  type = media_type
325
404
  meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD)
405
+
326
406
  (meth == POST && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
327
407
  end
328
408
 
@@ -377,8 +457,6 @@ module Rack
377
457
  # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
378
458
  def params
379
459
  self.GET.merge(self.POST)
380
- rescue EOFError
381
- self.GET.dup
382
460
  end
383
461
 
384
462
  # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
@@ -412,9 +490,7 @@ module Rack
412
490
  end
413
491
 
414
492
  def base_url
415
- url = "#{scheme}://#{host}"
416
- url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme]
417
- url
493
+ "#{scheme}://#{host_with_port}"
418
494
  end
419
495
 
420
496
  # Tries to return a remake of the original request URL as a string.
@@ -471,6 +547,20 @@ module Rack
471
547
 
472
548
  def default_session; {}; end
473
549
 
550
+ # Assist with compatibility when processing `X-Forwarded-For`.
551
+ def wrap_ipv6(host)
552
+ # Even thought IPv6 addresses should be wrapped in square brackets,
553
+ # sometimes this is not done in various legacy/underspecified headers.
554
+ # So we try to fix this situation for compatibility reasons.
555
+
556
+ # Try to detect IPv6 addresses which aren't escaped yet:
557
+ if !host.start_with?('[') && host.count(':') > 1
558
+ "[#{host}]"
559
+ else
560
+ host
561
+ end
562
+ end
563
+
474
564
  def parse_http_accept_header(header)
475
565
  header.to_s.split(/\s*,\s*/).map do |part|
476
566
  attribute, parameters = part.split(/\s*;\s*/, 2)
@@ -494,27 +584,39 @@ module Rack
494
584
  Rack::Multipart.extract_multipart(self, query_parser)
495
585
  end
496
586
 
497
- def split_ip_addresses(ip_addresses)
498
- ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
499
- end
500
-
501
- def strip_port(ip_address)
502
- # IPv6 format with optional port: "[2001:db8:cafe::17]:47011"
503
- # returns: "2001:db8:cafe::17"
504
- sep_start = ip_address.index('[')
505
- sep_end = ip_address.index(']')
506
- if (sep_start && sep_end)
507
- return ip_address[sep_start + 1, sep_end - 1]
508
- end
509
-
510
- # IPv4 format with optional port: "192.0.2.43:47011"
511
- # returns: "192.0.2.43"
512
- sep = ip_address.index(':')
513
- if (sep && ip_address.count(':') == 1)
514
- return ip_address[0, sep]
587
+ def split_header(value)
588
+ value ? value.strip.split(/[,\s]+/) : []
589
+ end
590
+
591
+ AUTHORITY = /
592
+ # The host:
593
+ (?<host>
594
+ # An IPv6 address:
595
+ (\[(?<ip6>.*)\])
596
+ |
597
+ # An IPv4 address:
598
+ (?<ip4>[\d\.]+)
599
+ |
600
+ # A hostname:
601
+ (?<name>[a-zA-Z0-9\.\-]+)
602
+ )
603
+ # The optional port:
604
+ (:(?<port>\d+))?
605
+ /x
606
+
607
+ private_constant :AUTHORITY
608
+
609
+ def split_authority(authority)
610
+ if match = AUTHORITY.match(authority)
611
+ if address = match[:ip6]
612
+ return match[:host], address, match[:port]&.to_i
613
+ else
614
+ return match[:host], match[:host], match[:port]&.to_i
615
+ end
515
616
  end
516
617
 
517
- ip_address
618
+ # Give up!
619
+ return authority, authority, nil
518
620
  end
519
621
 
520
622
  def reject_trusted_ip_addresses(ip_addresses)
@@ -539,12 +641,6 @@ module Rack
539
641
  end
540
642
  end
541
643
  end
542
-
543
- def extract_port(uri)
544
- if (colon_index = uri.index(':'))
545
- uri[colon_index + 1, uri.length]
546
- end
547
- end
548
644
  end
549
645
 
550
646
  include Env