rack 2.1.0 → 2.2.2

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 +126 -6
  3. data/CONTRIBUTING.md +136 -0
  4. data/README.rdoc +83 -39
  5. data/Rakefile +14 -7
  6. data/{SPEC → SPEC.rdoc} +26 -1
  7. data/lib/rack.rb +7 -16
  8. data/lib/rack/auth/abstract/request.rb +0 -2
  9. data/lib/rack/auth/basic.rb +3 -3
  10. data/lib/rack/auth/digest/md5.rb +4 -4
  11. data/lib/rack/auth/digest/request.rb +3 -3
  12. data/lib/rack/body_proxy.rb +13 -9
  13. data/lib/rack/builder.rb +78 -8
  14. data/lib/rack/cascade.rb +23 -8
  15. data/lib/rack/chunked.rb +48 -23
  16. data/lib/rack/common_logger.rb +25 -18
  17. data/lib/rack/conditional_get.rb +18 -16
  18. data/lib/rack/content_length.rb +6 -7
  19. data/lib/rack/content_type.rb +3 -4
  20. data/lib/rack/deflater.rb +49 -35
  21. data/lib/rack/directory.rb +77 -60
  22. data/lib/rack/etag.rb +2 -3
  23. data/lib/rack/events.rb +15 -18
  24. data/lib/rack/file.rb +1 -2
  25. data/lib/rack/files.rb +97 -57
  26. data/lib/rack/handler/cgi.rb +1 -4
  27. data/lib/rack/handler/fastcgi.rb +1 -3
  28. data/lib/rack/handler/lsws.rb +1 -3
  29. data/lib/rack/handler/scgi.rb +1 -3
  30. data/lib/rack/handler/thin.rb +1 -3
  31. data/lib/rack/handler/webrick.rb +12 -5
  32. data/lib/rack/head.rb +0 -2
  33. data/lib/rack/lint.rb +57 -14
  34. data/lib/rack/lobster.rb +3 -5
  35. data/lib/rack/lock.rb +0 -1
  36. data/lib/rack/mock.rb +22 -4
  37. data/lib/rack/multipart.rb +1 -1
  38. data/lib/rack/multipart/generator.rb +11 -6
  39. data/lib/rack/multipart/parser.rb +10 -18
  40. data/lib/rack/multipart/uploaded_file.rb +13 -7
  41. data/lib/rack/query_parser.rb +7 -8
  42. data/lib/rack/recursive.rb +1 -1
  43. data/lib/rack/reloader.rb +1 -3
  44. data/lib/rack/request.rb +182 -76
  45. data/lib/rack/response.rb +62 -19
  46. data/lib/rack/rewindable_input.rb +0 -1
  47. data/lib/rack/runtime.rb +3 -3
  48. data/lib/rack/sendfile.rb +0 -3
  49. data/lib/rack/server.rb +9 -10
  50. data/lib/rack/session/abstract/id.rb +23 -28
  51. data/lib/rack/session/cookie.rb +1 -3
  52. data/lib/rack/session/pool.rb +1 -1
  53. data/lib/rack/show_exceptions.rb +6 -8
  54. data/lib/rack/show_status.rb +5 -7
  55. data/lib/rack/static.rb +13 -6
  56. data/lib/rack/tempfile_reaper.rb +0 -2
  57. data/lib/rack/urlmap.rb +1 -4
  58. data/lib/rack/utils.rb +58 -54
  59. data/lib/rack/version.rb +29 -0
  60. data/rack.gemspec +31 -29
  61. metadata +11 -12
@@ -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,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}"
@@ -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
@@ -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,101 @@ 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'))
278
- remote_addrs = reject_trusted_ip_addresses(remote_addrs)
355
+ remote_addresses = split_header(get_header('REMOTE_ADDR'))
356
+ external_addresses = reject_trusted_ip_addresses(remote_addresses)
279
357
 
280
- return remote_addrs.first if remote_addrs.any?
358
+ unless external_addresses.empty?
359
+ return external_addresses.first
360
+ end
281
361
 
282
- forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))
283
- .map { |ip| strip_port(ip) }
362
+ if forwarded_for = self.forwarded_for
363
+ unless forwarded_for.empty?
364
+ # The forwarded for addresses are ordered: client, proxy1, proxy2.
365
+ # So we reject all the trusted addresses (proxy*) and return the
366
+ # last client. Or if we trust everyone, we just return the first
367
+ # address.
368
+ return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first
369
+ end
370
+ end
284
371
 
285
- return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
372
+ # If all the addresses are trusted, and we aren't forwarded, just return
373
+ # the first remote address, which represents the source of the request.
374
+ remote_addresses.first
286
375
  end
287
376
 
288
377
  # The media type (type/subtype) portion of the CONTENT_TYPE header
@@ -323,6 +412,7 @@ module Rack
323
412
  def form_data?
324
413
  type = media_type
325
414
  meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD)
415
+
326
416
  (meth == POST && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
327
417
  end
328
418
 
@@ -377,8 +467,6 @@ module Rack
377
467
  # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
378
468
  def params
379
469
  self.GET.merge(self.POST)
380
- rescue EOFError
381
- self.GET.dup
382
470
  end
383
471
 
384
472
  # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
@@ -412,9 +500,7 @@ module Rack
412
500
  end
413
501
 
414
502
  def base_url
415
- url = "#{scheme}://#{host}"
416
- url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme]
417
- url
503
+ "#{scheme}://#{host_with_port}"
418
504
  end
419
505
 
420
506
  # Tries to return a remake of the original request URL as a string.
@@ -471,6 +557,20 @@ module Rack
471
557
 
472
558
  def default_session; {}; end
473
559
 
560
+ # Assist with compatibility when processing `X-Forwarded-For`.
561
+ def wrap_ipv6(host)
562
+ # Even thought IPv6 addresses should be wrapped in square brackets,
563
+ # sometimes this is not done in various legacy/underspecified headers.
564
+ # So we try to fix this situation for compatibility reasons.
565
+
566
+ # Try to detect IPv6 addresses which aren't escaped yet:
567
+ if !host.start_with?('[') && host.count(':') > 1
568
+ "[#{host}]"
569
+ else
570
+ host
571
+ end
572
+ end
573
+
474
574
  def parse_http_accept_header(header)
475
575
  header.to_s.split(/\s*,\s*/).map do |part|
476
576
  attribute, parameters = part.split(/\s*;\s*/, 2)
@@ -494,27 +594,39 @@ module Rack
494
594
  Rack::Multipart.extract_multipart(self, query_parser)
495
595
  end
496
596
 
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]
597
+ def split_header(value)
598
+ value ? value.strip.split(/[,\s]+/) : []
599
+ end
600
+
601
+ AUTHORITY = /^
602
+ # The host:
603
+ (?<host>
604
+ # An IPv6 address:
605
+ (\[(?<ip6>.*)\])
606
+ |
607
+ # An IPv4 address:
608
+ (?<ip4>[\d\.]+)
609
+ |
610
+ # A hostname:
611
+ (?<name>[a-zA-Z0-9\.\-]+)
612
+ )
613
+ # The optional port:
614
+ (:(?<port>\d+))?
615
+ $/x
616
+
617
+ private_constant :AUTHORITY
618
+
619
+ def split_authority(authority)
620
+ if match = AUTHORITY.match(authority)
621
+ if address = match[:ip6]
622
+ return match[:host], address, match[:port]&.to_i
623
+ else
624
+ return match[:host], match[:host], match[:port]&.to_i
625
+ end
515
626
  end
516
627
 
517
- ip_address
628
+ # Give up!
629
+ return authority, authority, nil
518
630
  end
519
631
 
520
632
  def reject_trusted_ip_addresses(ip_addresses)
@@ -539,12 +651,6 @@ module Rack
539
651
  end
540
652
  end
541
653
  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
654
  end
549
655
 
550
656
  include Env