rack 2.0.8 → 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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +690 -0
  3. data/CONTRIBUTING.md +136 -0
  4. data/{COPYING → MIT-LICENSE} +4 -2
  5. data/README.rdoc +152 -148
  6. data/Rakefile +37 -23
  7. data/{SPEC → SPEC.rdoc} +29 -5
  8. data/bin/rackup +1 -0
  9. data/example/lobster.ru +2 -0
  10. data/example/protectedlobster.rb +3 -1
  11. data/example/protectedlobster.ru +2 -0
  12. data/lib/rack.rb +67 -73
  13. data/lib/rack/auth/abstract/handler.rb +3 -1
  14. data/lib/rack/auth/abstract/request.rb +1 -1
  15. data/lib/rack/auth/basic.rb +7 -4
  16. data/lib/rack/auth/digest/md5.rb +13 -11
  17. data/lib/rack/auth/digest/nonce.rb +6 -3
  18. data/lib/rack/auth/digest/params.rb +4 -2
  19. data/lib/rack/auth/digest/request.rb +5 -3
  20. data/lib/rack/body_proxy.rb +15 -14
  21. data/lib/rack/builder.rb +116 -23
  22. data/lib/rack/cascade.rb +28 -12
  23. data/lib/rack/chunked.rb +68 -20
  24. data/lib/rack/common_logger.rb +33 -25
  25. data/lib/rack/conditional_get.rb +20 -16
  26. data/lib/rack/config.rb +2 -0
  27. data/lib/rack/content_length.rb +8 -7
  28. data/lib/rack/content_type.rb +5 -4
  29. data/lib/rack/core_ext/regexp.rb +14 -0
  30. data/lib/rack/deflater.rb +59 -34
  31. data/lib/rack/directory.rb +84 -64
  32. data/lib/rack/etag.rb +5 -4
  33. data/lib/rack/events.rb +19 -20
  34. data/lib/rack/file.rb +4 -173
  35. data/lib/rack/files.rb +218 -0
  36. data/lib/rack/handler.rb +7 -2
  37. data/lib/rack/handler/cgi.rb +2 -3
  38. data/lib/rack/handler/fastcgi.rb +4 -4
  39. data/lib/rack/handler/lsws.rb +3 -3
  40. data/lib/rack/handler/scgi.rb +9 -8
  41. data/lib/rack/handler/thin.rb +3 -3
  42. data/lib/rack/handler/webrick.rb +15 -6
  43. data/lib/rack/head.rb +1 -1
  44. data/lib/rack/lint.rb +71 -25
  45. data/lib/rack/lobster.rb +10 -10
  46. data/lib/rack/lock.rb +2 -1
  47. data/lib/rack/logger.rb +2 -0
  48. data/lib/rack/media_type.rb +10 -5
  49. data/lib/rack/method_override.rb +4 -2
  50. data/lib/rack/mime.rb +9 -1
  51. data/lib/rack/mock.rb +97 -20
  52. data/lib/rack/multipart.rb +6 -4
  53. data/lib/rack/multipart/generator.rb +17 -13
  54. data/lib/rack/multipart/parser.rb +54 -56
  55. data/lib/rack/multipart/uploaded_file.rb +15 -7
  56. data/lib/rack/null_logger.rb +2 -0
  57. data/lib/rack/query_parser.rb +53 -28
  58. data/lib/rack/recursive.rb +7 -5
  59. data/lib/rack/reloader.rb +8 -4
  60. data/lib/rack/request.rb +220 -61
  61. data/lib/rack/response.rb +127 -44
  62. data/lib/rack/rewindable_input.rb +4 -3
  63. data/lib/rack/runtime.rb +6 -4
  64. data/lib/rack/sendfile.rb +13 -9
  65. data/lib/rack/server.rb +95 -24
  66. data/lib/rack/session/abstract/id.rb +36 -23
  67. data/lib/rack/session/cookie.rb +11 -12
  68. data/lib/rack/session/memcache.rb +4 -93
  69. data/lib/rack/session/pool.rb +5 -3
  70. data/lib/rack/show_exceptions.rb +21 -17
  71. data/lib/rack/show_status.rb +9 -9
  72. data/lib/rack/static.rb +23 -11
  73. data/lib/rack/tempfile_reaper.rb +1 -1
  74. data/lib/rack/urlmap.rb +12 -6
  75. data/lib/rack/utils.rb +98 -109
  76. data/lib/rack/version.rb +29 -0
  77. data/rack.gemspec +40 -28
  78. metadata +36 -177
  79. data/HISTORY.md +0 -505
  80. data/test/builder/an_underscore_app.rb +0 -5
  81. data/test/builder/anything.rb +0 -5
  82. data/test/builder/comment.ru +0 -4
  83. data/test/builder/end.ru +0 -5
  84. data/test/builder/line.ru +0 -1
  85. data/test/builder/options.ru +0 -2
  86. data/test/cgi/assets/folder/test.js +0 -1
  87. data/test/cgi/assets/fonts/font.eot +0 -1
  88. data/test/cgi/assets/images/image.png +0 -1
  89. data/test/cgi/assets/index.html +0 -1
  90. data/test/cgi/assets/javascripts/app.js +0 -1
  91. data/test/cgi/assets/stylesheets/app.css +0 -1
  92. data/test/cgi/lighttpd.conf +0 -26
  93. data/test/cgi/rackup_stub.rb +0 -6
  94. data/test/cgi/sample_rackup.ru +0 -5
  95. data/test/cgi/test +0 -9
  96. data/test/cgi/test+directory/test+file +0 -1
  97. data/test/cgi/test.fcgi +0 -9
  98. data/test/cgi/test.gz +0 -0
  99. data/test/cgi/test.ru +0 -5
  100. data/test/gemloader.rb +0 -10
  101. data/test/helper.rb +0 -34
  102. data/test/multipart/bad_robots +0 -259
  103. data/test/multipart/binary +0 -0
  104. data/test/multipart/content_type_and_no_filename +0 -6
  105. data/test/multipart/empty +0 -10
  106. data/test/multipart/fail_16384_nofile +0 -814
  107. data/test/multipart/file1.txt +0 -1
  108. data/test/multipart/filename_and_modification_param +0 -7
  109. data/test/multipart/filename_and_no_name +0 -6
  110. data/test/multipart/filename_with_encoded_words +0 -7
  111. data/test/multipart/filename_with_escaped_quotes +0 -6
  112. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  113. data/test/multipart/filename_with_null_byte +0 -7
  114. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  115. data/test/multipart/filename_with_single_quote +0 -7
  116. data/test/multipart/filename_with_unescaped_percentages +0 -6
  117. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  118. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  119. data/test/multipart/filename_with_unescaped_quotes +0 -6
  120. data/test/multipart/ie +0 -6
  121. data/test/multipart/invalid_character +0 -6
  122. data/test/multipart/mixed_files +0 -21
  123. data/test/multipart/nested +0 -10
  124. data/test/multipart/none +0 -9
  125. data/test/multipart/quoted +0 -15
  126. data/test/multipart/rack-logo.png +0 -0
  127. data/test/multipart/semicolon +0 -6
  128. data/test/multipart/text +0 -15
  129. data/test/multipart/three_files_three_fields +0 -31
  130. data/test/multipart/unity3d_wwwform +0 -11
  131. data/test/multipart/webkit +0 -32
  132. data/test/rackup/config.ru +0 -31
  133. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  134. data/test/spec_auth_basic.rb +0 -89
  135. data/test/spec_auth_digest.rb +0 -260
  136. data/test/spec_body_proxy.rb +0 -85
  137. data/test/spec_builder.rb +0 -233
  138. data/test/spec_cascade.rb +0 -63
  139. data/test/spec_cgi.rb +0 -84
  140. data/test/spec_chunked.rb +0 -103
  141. data/test/spec_common_logger.rb +0 -95
  142. data/test/spec_conditional_get.rb +0 -103
  143. data/test/spec_config.rb +0 -23
  144. data/test/spec_content_length.rb +0 -86
  145. data/test/spec_content_type.rb +0 -46
  146. data/test/spec_deflater.rb +0 -375
  147. data/test/spec_directory.rb +0 -148
  148. data/test/spec_etag.rb +0 -108
  149. data/test/spec_events.rb +0 -133
  150. data/test/spec_fastcgi.rb +0 -85
  151. data/test/spec_file.rb +0 -264
  152. data/test/spec_handler.rb +0 -57
  153. data/test/spec_head.rb +0 -46
  154. data/test/spec_lint.rb +0 -515
  155. data/test/spec_lobster.rb +0 -59
  156. data/test/spec_lock.rb +0 -204
  157. data/test/spec_logger.rb +0 -24
  158. data/test/spec_media_type.rb +0 -42
  159. data/test/spec_method_override.rb +0 -110
  160. data/test/spec_mime.rb +0 -51
  161. data/test/spec_mock.rb +0 -359
  162. data/test/spec_multipart.rb +0 -722
  163. data/test/spec_null_logger.rb +0 -21
  164. data/test/spec_recursive.rb +0 -75
  165. data/test/spec_request.rb +0 -1407
  166. data/test/spec_response.rb +0 -510
  167. data/test/spec_rewindable_input.rb +0 -128
  168. data/test/spec_runtime.rb +0 -50
  169. data/test/spec_sendfile.rb +0 -125
  170. data/test/spec_server.rb +0 -193
  171. data/test/spec_session_abstract_id.rb +0 -31
  172. data/test/spec_session_abstract_session_hash.rb +0 -45
  173. data/test/spec_session_cookie.rb +0 -442
  174. data/test/spec_session_memcache.rb +0 -357
  175. data/test/spec_session_pool.rb +0 -247
  176. data/test/spec_show_exceptions.rb +0 -93
  177. data/test/spec_show_status.rb +0 -104
  178. data/test/spec_static.rb +0 -184
  179. data/test/spec_tempfile_reaper.rb +0 -64
  180. data/test/spec_thin.rb +0 -96
  181. data/test/spec_urlmap.rb +0 -237
  182. data/test/spec_utils.rb +0 -742
  183. data/test/spec_version.rb +0 -11
  184. data/test/spec_webrick.rb +0 -206
  185. data/test/static/another/index.html +0 -1
  186. data/test/static/foo.html +0 -1
  187. data/test/static/index.html +0 -1
  188. data/test/testrequest.rb +0 -78
  189. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  190. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  module Multipart
3
5
  class UploadedFile
@@ -7,17 +9,23 @@ module Rack
7
9
  # The content type of the "uploaded" file
8
10
  attr_accessor :content_type
9
11
 
10
- def initialize(path, content_type = "text/plain", binary = false)
11
- 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
12
24
  @content_type = content_type
13
- @original_filename = ::File.basename(path)
14
- @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
15
- @tempfile.binmode if binary
16
- FileUtils.copy_file(path, @tempfile.path)
17
25
  end
18
26
 
19
27
  def path
20
- @tempfile.path
28
+ @tempfile.path if @tempfile.respond_to?(:path)
21
29
  end
22
30
  alias_method :local_path, :path
23
31
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class NullLogger
3
5
  def initialize(app)
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class QueryParser
5
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
6
+
3
7
  DEFAULT_SEP = /[&;] */n
4
8
  COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
5
9
 
@@ -36,7 +40,7 @@ module Rack
36
40
 
37
41
  (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
38
42
  next if p.empty?
39
- k, v = p.split('='.freeze, 2).map!(&unescaper)
43
+ k, v = p.split('=', 2).map!(&unescaper)
40
44
 
41
45
  if cur = params[k]
42
46
  if cur.class == Array
@@ -49,7 +53,7 @@ module Rack
49
53
  end
50
54
  end
51
55
 
52
- return params.to_params_hash
56
+ return params.to_h
53
57
  end
54
58
 
55
59
  # parse_nested_query expands a query string into structural types. Supported
@@ -58,18 +62,19 @@ module Rack
58
62
  # ParameterTypeError is raised. Users are encouraged to return a 400 in this
59
63
  # case.
60
64
  def parse_nested_query(qs, d = nil)
61
- return {} if qs.nil? || qs.empty?
62
65
  params = make_params
63
66
 
64
- (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
65
- k, v = p.split('='.freeze, 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) }
66
70
 
67
- normalize_params(params, k, v, param_depth_limit)
71
+ normalize_params(params, k, v, param_depth_limit)
72
+ end
68
73
  end
69
74
 
70
- return params.to_params_hash
75
+ return params.to_h
71
76
  rescue ArgumentError => e
72
- raise InvalidParameterError, e.message
77
+ raise InvalidParameterError, e.message, e.backtrace
73
78
  end
74
79
 
75
80
  # normalize_params recursively expands parameters into structural types. If
@@ -79,22 +84,22 @@ module Rack
79
84
  raise RangeError if depth <= 0
80
85
 
81
86
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
82
- k = $1 || ''.freeze
83
- after = $' || ''.freeze
87
+ k = $1 || ''
88
+ after = $' || ''
84
89
 
85
90
  if k.empty?
86
- if !v.nil? && name == "[]".freeze
91
+ if !v.nil? && name == "[]"
87
92
  return Array(v)
88
93
  else
89
94
  return
90
95
  end
91
96
  end
92
97
 
93
- if after == ''.freeze
98
+ if after == ''
94
99
  params[k] = v
95
- elsif after == "[".freeze
100
+ elsif after == "["
96
101
  params[name] = v
97
- elsif after == "[]".freeze
102
+ elsif after == "[]"
98
103
  params[k] ||= []
99
104
  raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
100
105
  params[k] << v
@@ -135,7 +140,7 @@ module Rack
135
140
  end
136
141
 
137
142
  def params_hash_has_key?(hash, key)
138
- return false if key =~ /\[\]/
143
+ return false if /\[\]/.match?(key)
139
144
 
140
145
  key.split(/[\[\]]+/).inject(hash) do |h, part|
141
146
  next h if part == ''
@@ -171,22 +176,42 @@ module Rack
171
176
  @params.key?(key)
172
177
  end
173
178
 
174
- def to_params_hash
175
- hash = @params
176
- hash.keys.each do |key|
177
- value = hash[key]
178
- if value.kind_of?(self.class)
179
- if value.object_id == self.object_id
180
- hash[key] = hash
181
- else
182
- hash[key] = value.to_params_hash
183
- end
184
- elsif value.kind_of?(Array)
185
- value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
179
+ # Recursively unwraps nested `Params` objects and constructs an object
180
+ # of the same shape, but using the objects' internal representations
181
+ # (Ruby hashes) in place of the objects. The result is a hash consisting
182
+ # purely of Ruby primitives.
183
+ #
184
+ # Mutation warning!
185
+ #
186
+ # 1. This method mutates the internal representation of the `Params`
187
+ # objects in order to save object allocations.
188
+ #
189
+ # 2. The value you get back is a reference to the internal hash
190
+ # representation, not a copy.
191
+ #
192
+ # 3. Because the `Params` object's internal representation is mutable
193
+ # through the `#[]=` method, it is not thread safe. The result of
194
+ # getting the hash representation while another thread is adding a
195
+ # key to it is non-deterministic.
196
+ #
197
+ def to_h
198
+ @params.each do |key, value|
199
+ case value
200
+ when self
201
+ # Handle circular references gracefully.
202
+ @params[key] = @params
203
+ when Params
204
+ @params[key] = value.to_h
205
+ when Array
206
+ value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
207
+ else
208
+ # Ignore anything that is not a `Params` object or
209
+ # a collection that can contain one.
186
210
  end
187
211
  end
188
- hash
212
+ @params
189
213
  end
214
+ alias_method :to_params_hash, :to_h
190
215
  end
191
216
  end
192
217
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
 
3
5
  module Rack
@@ -10,14 +12,14 @@ module Rack
10
12
  class ForwardRequest < Exception
11
13
  attr_reader :url, :env
12
14
 
13
- def initialize(url, env={})
15
+ def initialize(url, env = {})
14
16
  @url = URI(url)
15
17
  @env = env
16
18
 
17
- @env[PATH_INFO] = @url.path
18
- @env[QUERY_STRING] = @url.query if @url.query
19
- @env[HTTP_HOST] = @url.host if @url.host
20
- @env["HTTP_PORT"] = @url.port if @url.port
19
+ @env[PATH_INFO] = @url.path
20
+ @env[QUERY_STRING] = @url.query if @url.query
21
+ @env[HTTP_HOST] = @url.host if @url.host
22
+ @env[HTTP_PORT] = @url.port if @url.port
21
23
  @env[RACK_URL_SCHEME] = @url.scheme if @url.scheme
22
24
 
23
25
  super "forwarding to #{url}"
@@ -1,6 +1,8 @@
1
- # Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
2
- # Rack::Reloader is subject to the terms of an MIT-style license.
3
- # See COPYING or http://www.opensource.org/licenses/mit-license.php.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (C) 2009-2018 Michael Fellinger <m.fellinger@gmail.com>
4
+ # Rack::Reloader is subject to the terms of an MIT-style license.
5
+ # See MIT-LICENSE or https://opensource.org/licenses/MIT.
4
6
 
5
7
  require 'pathname'
6
8
 
@@ -20,6 +22,8 @@ module Rack
20
22
  # It is performing a check/reload cycle at the start of every request, but
21
23
  # also respects a cool down time, during which nothing will be done.
22
24
  class Reloader
25
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
26
+
23
27
  def initialize(app, cooldown = 10, backend = Stat)
24
28
  @app = app
25
29
  @cooldown = cooldown
@@ -69,7 +73,7 @@ module Rack
69
73
  paths = ['./', *$LOAD_PATH].uniq
70
74
 
71
75
  files.map{|file|
72
- next if file =~ /\.(so|bundle)$/ # cannot reload compiled files
76
+ next if /\.(so|bundle)$/.match?(file) # cannot reload compiled files
73
77
 
74
78
  found, stat = figure_path(file, paths)
75
79
  next unless found && stat && mtime = stat.mtime
@@ -1,5 +1,4 @@
1
- require 'rack/utils'
2
- require 'rack/media_type'
1
+ # frozen_string_literal: true
3
2
 
4
3
  module Rack
5
4
  # Rack::Request provides a convenient interface to a Rack
@@ -11,7 +10,18 @@ module Rack
11
10
  # req.params["data"]
12
11
 
13
12
  class Request
14
- SCHEME_WHITELIST = %w(https http).freeze
13
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
14
+
15
+ class << self
16
+ attr_accessor :ip_filter
17
+ end
18
+
19
+ self.ip_filter = lambda { |ip| /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i.match?(ip) }
20
+ ALLOWED_SCHEMES = %w(https http).freeze
21
+ SCHEME_WHITELIST = ALLOWED_SCHEMES
22
+ if Object.respond_to?(:deprecate_constant)
23
+ deprecate_constant :SCHEME_WHITELIST
24
+ end
15
25
 
16
26
  def initialize(env)
17
27
  @params = nil
@@ -78,7 +88,7 @@ module Rack
78
88
  # assert_equal 'image/png,*/*', request.get_header('Accept')
79
89
  #
80
90
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
81
- def add_header key, v
91
+ def add_header(key, v)
82
92
  if v.nil?
83
93
  get_header key
84
94
  elsif has_header? key
@@ -100,7 +110,7 @@ module Rack
100
110
 
101
111
  module Helpers
102
112
  # The set of form-data media-types. Requests that do not indicate
103
- # one of the media types presents in this list will not be eligible
113
+ # one of the media types present in this list will not be eligible
104
114
  # for form-data / param parsing.
105
115
  FORM_DATA_MEDIA_TYPES = [
106
116
  'application/x-www-form-urlencoded',
@@ -108,7 +118,7 @@ module Rack
108
118
  ]
109
119
 
110
120
  # The set of media-types. Requests that do not indicate
111
- # one of the media types presents in this list will not be eligible
121
+ # one of the media types present in this list will not be eligible
112
122
  # for param parsing like soap attachments or generic multiparts
113
123
  PARSEABLE_DATA_MEDIA_TYPES = [
114
124
  'multipart/related',
@@ -119,11 +129,23 @@ module Rack
119
129
  # to include the port in a generated URI.
120
130
  DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
121
131
 
122
- HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'.freeze
123
- HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'.freeze
124
- HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'.freeze
125
- HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'.freeze
126
- HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'.freeze
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.
139
+ HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
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'
127
149
 
128
150
  def body; get_header(RACK_INPUT) end
129
151
  def script_name; get_header(SCRIPT_NAME).to_s end
@@ -159,10 +181,10 @@ module Rack
159
181
  def delete?; request_method == DELETE end
160
182
 
161
183
  # Checks the HTTP request method (or verb) to see if it was of type GET
162
- def get?; request_method == GET end
184
+ def get?; request_method == GET end
163
185
 
164
186
  # Checks the HTTP request method (or verb) to see if it was of type HEAD
165
- def head?; request_method == HEAD end
187
+ def head?; request_method == HEAD end
166
188
 
167
189
  # Checks the HTTP request method (or verb) to see if it was of type OPTIONS
168
190
  def options?; request_method == OPTIONS end
@@ -197,19 +219,52 @@ module Rack
197
219
  end
198
220
  end
199
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.
200
227
  def authority
201
- 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
202
254
  end
203
255
 
204
256
  def cookies
205
- hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
206
- 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)
207
266
  end
208
- string = get_header HTTP_COOKIE
209
267
 
210
- return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
211
- hash.replace Utils.parse_cookies_header get_header HTTP_COOKIE
212
- set_header(RACK_REQUEST_COOKIE_STRING, string)
213
268
  hash
214
269
  end
215
270
 
@@ -222,46 +277,101 @@ module Rack
222
277
  get_header("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
223
278
  end
224
279
 
225
- def host_with_port
226
- if forwarded = get_header(HTTP_X_FORWARDED_HOST)
227
- 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
228
290
  else
229
- get_header(HTTP_HOST) || "#{get_header(SERVER_NAME) || get_header(SERVER_ADDR)}:#{get_header(SERVER_PORT)}"
291
+ authority
230
292
  end
231
293
  end
232
294
 
295
+ # Returns a formatted host, suitable for being used in a URI.
233
296
  def host
234
- # Remove port number.
235
- host_with_port.to_s.sub(/:\d+\z/, '')
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]
236
306
  end
237
307
 
238
308
  def port
239
- if port = host_with_port.split(/:/)[1]
240
- port.to_i
241
- elsif port = get_header(HTTP_X_FORWARDED_PORT)
242
- port.to_i
243
- elsif has_header?(HTTP_X_FORWARDED_HOST)
244
- DEFAULT_PORTS[scheme]
245
- elsif has_header?(HTTP_X_FORWARDED_PROTO)
246
- DEFAULT_PORTS[get_header(HTTP_X_FORWARDED_PROTO).split(',')[0]]
247
- else
248
- 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)
249
347
  end
250
348
  end
251
349
 
252
350
  def ssl?
253
- scheme == 'https'
351
+ scheme == 'https' || scheme == 'wss'
254
352
  end
255
353
 
256
354
  def ip
257
- remote_addrs = split_ip_addresses(get_header('REMOTE_ADDR'))
258
- 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)
259
357
 
260
- return remote_addrs.first if remote_addrs.any?
358
+ unless external_addresses.empty?
359
+ return external_addresses.first
360
+ end
261
361
 
262
- forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))
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
263
371
 
264
- 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
265
375
  end
266
376
 
267
377
  # The media type (type/subtype) portion of the CONTENT_TYPE header
@@ -302,6 +412,7 @@ module Rack
302
412
  def form_data?
303
413
  type = media_type
304
414
  meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD)
415
+
305
416
  (meth == POST && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
306
417
  end
307
418
 
@@ -337,7 +448,7 @@ module Rack
337
448
 
338
449
  # Fix for Safari Ajax postings that always append \0
339
450
  # form_vars.sub!(/\0\z/, '') # performance replacement:
340
- form_vars.slice!(-1) if form_vars[-1] == ?\0
451
+ form_vars.slice!(-1) if form_vars.end_with?("\0")
341
452
 
342
453
  set_header RACK_REQUEST_FORM_VARS, form_vars
343
454
  set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
@@ -356,8 +467,6 @@ module Rack
356
467
  # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
357
468
  def params
358
469
  self.GET.merge(self.POST)
359
- rescue EOFError
360
- self.GET.dup
361
470
  end
362
471
 
363
472
  # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
@@ -386,13 +495,12 @@ module Rack
386
495
  #
387
496
  # <tt>env['rack.input']</tt> is not touched.
388
497
  def delete_param(k)
389
- [ self.POST.delete(k), self.GET.delete(k) ].compact.first
498
+ post_value, get_value = self.POST.delete(k), self.GET.delete(k)
499
+ post_value || get_value
390
500
  end
391
501
 
392
502
  def base_url
393
- url = "#{scheme}://#{host}"
394
- url << ":#{port}" if port != DEFAULT_PORTS[scheme]
395
- url
503
+ "#{scheme}://#{host_with_port}"
396
504
  end
397
505
 
398
506
  # Tries to return a remake of the original request URL as a string.
@@ -417,7 +525,7 @@ module Rack
417
525
  end
418
526
 
419
527
  def trusted_proxy?(ip)
420
- ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i
528
+ Rack::Request.ip_filter.call(ip)
421
529
  end
422
530
 
423
531
  # shortcut for <tt>request.params[key]</tt>
@@ -449,6 +557,20 @@ module Rack
449
557
 
450
558
  def default_session; {}; end
451
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
+
452
574
  def parse_http_accept_header(header)
453
575
  header.to_s.split(/\s*,\s*/).map do |part|
454
576
  attribute, parameters = part.split(/\s*;\s*/, 2)
@@ -464,7 +586,7 @@ module Rack
464
586
  Utils.default_query_parser
465
587
  end
466
588
 
467
- def parse_query(qs, d='&')
589
+ def parse_query(qs, d = '&')
468
590
  query_parser.parse_nested_query(qs, d)
469
591
  end
470
592
 
@@ -472,8 +594,39 @@ module Rack
472
594
  Rack::Multipart.extract_multipart(self, query_parser)
473
595
  end
474
596
 
475
- def split_ip_addresses(ip_addresses)
476
- ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
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
626
+ end
627
+
628
+ # Give up!
629
+ return authority, authority, nil
477
630
  end
478
631
 
479
632
  def reject_trusted_ip_addresses(ip_addresses)
@@ -481,16 +634,22 @@ module Rack
481
634
  end
482
635
 
483
636
  def forwarded_scheme
484
- scheme_headers = [
485
- get_header(HTTP_X_FORWARDED_SCHEME),
486
- get_header(HTTP_X_FORWARDED_PROTO).to_s.split(',')[0]
487
- ]
637
+ allowed_scheme(get_header(HTTP_X_FORWARDED_SCHEME)) ||
638
+ allowed_scheme(extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO)))
639
+ end
488
640
 
489
- scheme_headers.each do |header|
490
- return header if SCHEME_WHITELIST.include?(header)
491
- end
641
+ def allowed_scheme(header)
642
+ header if ALLOWED_SCHEMES.include?(header)
643
+ end
492
644
 
493
- nil
645
+ def extract_proto_header(header)
646
+ if header
647
+ if (comma_index = header.index(','))
648
+ header[0, comma_index]
649
+ else
650
+ header
651
+ end
652
+ end
494
653
  end
495
654
  end
496
655