lib/rack/request.rb in rack-2.1.4.4 vs lib/rack/request.rb in rack-2.2.0
- old
+ new
@@ -1,23 +1,18 @@
# frozen_string_literal: true
-require 'rack/utils'
-require 'rack/media_type'
-
-require_relative 'core_ext/regexp'
-
module Rack
# Rack::Request provides a convenient interface to a Rack
# environment. It is stateless, the environment +env+ passed to the
# constructor will be directly modified.
#
# req = Rack::Request.new(env)
# req.post?
# req.params["data"]
class Request
- using ::Rack::RegexpExtensions
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
class << self
attr_accessor :ip_filter
end
@@ -91,11 +86,11 @@
# request.add_header 'Accept', '*/*'
#
# assert_equal 'image/png,*/*', request.get_header('Accept')
#
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
- def add_header key, v
+ def add_header(key, v)
if v.nil?
get_header key
elsif has_header? key
set_header key, "#{get_header key},#{v}"
else
@@ -132,16 +127,28 @@
# Default ports depending on scheme. Used to decide whether or not
# to include the port in a generated URI.
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
+ # The address of the client which connected to the proxy.
+ HTTP_X_FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR'
+
+ # The contents of the host/:authority header sent to the proxy.
+ HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
+
+ # The value of the scheme sent to the proxy.
HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
- HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
- HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
- HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
- HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
+ # The protocol used to connect to the proxy.
+ HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
+
+ # The port used to connect to the proxy.
+ HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
+
+ # Another way for specifing https scheme was used.
+ HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
+
def body; get_header(RACK_INPUT) end
def script_name; get_header(SCRIPT_NAME).to_s end
def script_name=(s); set_header(SCRIPT_NAME, s.to_s) end
def path_info; get_header(PATH_INFO).to_s end
@@ -210,23 +217,56 @@
else
get_header(RACK_URL_SCHEME)
end
end
+ # The authority of the incoming request as defined by RFC3976.
+ # https://tools.ietf.org/html/rfc3986#section-3.2
+ #
+ # In HTTP/1, this is the `host` header.
+ # In HTTP/2, this is the `:authority` pseudo-header.
def authority
- get_header(SERVER_NAME) + ':' + get_header(SERVER_PORT)
+ forwarded_authority || host_authority || server_authority
end
+ # The authority as defined by the `SERVER_NAME` and `SERVER_PORT`
+ # variables.
+ def server_authority
+ host = self.server_name
+ port = self.server_port
+
+ if host
+ if port
+ "#{host}:#{port}"
+ else
+ host
+ end
+ end
+ end
+
+ def server_name
+ get_header(SERVER_NAME)
+ end
+
+ def server_port
+ if port = get_header(SERVER_PORT)
+ Integer(port)
+ end
+ end
+
def cookies
- hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
- set_header(k, {})
+ hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |key|
+ set_header(key, {})
end
- string = get_header HTTP_COOKIE
- return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
- hash.replace Utils.parse_cookies_header string
- set_header(RACK_REQUEST_COOKIE_STRING, string)
+ string = get_header(HTTP_COOKIE)
+
+ unless string == get_header(RACK_REQUEST_COOKIE_STRING)
+ hash.replace Utils.parse_cookies_header(string)
+ set_header(RACK_REQUEST_COOKIE_STRING, string)
+ end
+
hash
end
def content_type
content_type = get_header('CONTENT_TYPE')
@@ -235,56 +275,95 @@
def xhr?
get_header("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
end
- def host_with_port
- if forwarded = get_header(HTTP_X_FORWARDED_HOST)
- forwarded.split(/,\s?/).last
+ # The `HTTP_HOST` header.
+ def host_authority
+ get_header(HTTP_HOST)
+ end
+
+ def host_with_port(authority = self.authority)
+ host, _, port = split_authority(authority)
+
+ if port == DEFAULT_PORTS[self.scheme]
+ host
else
- get_header(HTTP_HOST) || "#{get_header(SERVER_NAME) || get_header(SERVER_ADDR)}:#{get_header(SERVER_PORT)}"
+ authority
end
end
+ # Returns a formatted host, suitable for being used in a URI.
def host
- # Remove port number.
- h = host_with_port
- if colon_index = h.index(":")
- h[0, colon_index]
- else
- h
- end
+ split_authority(self.authority)[0]
end
+ # Returns an address suitable for being to resolve to an address.
+ # In the case of a domain name or IPv4 address, the result is the same
+ # as +host+. In the case of IPv6 or future address formats, the square
+ # brackets are removed.
+ def hostname
+ split_authority(self.authority)[1]
+ end
+
def port
- if port = extract_port(host_with_port)
- port.to_i
- elsif port = get_header(HTTP_X_FORWARDED_PORT)
- port.to_i
- elsif has_header?(HTTP_X_FORWARDED_HOST)
- DEFAULT_PORTS[scheme]
- elsif has_header?(HTTP_X_FORWARDED_PROTO)
- DEFAULT_PORTS[extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO))]
- else
- get_header(SERVER_PORT).to_i
+ if authority = self.authority
+ _, _, port = split_authority(self.authority)
+
+ if port
+ return port
+ end
end
+
+ if forwarded_port = self.forwarded_port
+ return forwarded_port.first
+ end
+
+ if scheme = self.scheme
+ if port = DEFAULT_PORTS[self.scheme]
+ return port
+ end
+ end
+
+ self.server_port
end
+ def forwarded_for
+ if value = get_header(HTTP_X_FORWARDED_FOR)
+ split_header(value).map do |authority|
+ split_authority(wrap_ipv6(authority))[1]
+ end
+ end
+ end
+
+ def forwarded_port
+ if value = get_header(HTTP_X_FORWARDED_PORT)
+ split_header(value).map(&:to_i)
+ end
+ end
+
+ def forwarded_authority
+ if value = get_header(HTTP_X_FORWARDED_HOST)
+ wrap_ipv6(split_header(value).first)
+ end
+ end
+
def ssl?
- scheme == 'https'
+ scheme == 'https' || scheme == 'wss'
end
def ip
- remote_addrs = split_ip_addresses(get_header('REMOTE_ADDR'))
+ remote_addrs = split_header(get_header('REMOTE_ADDR'))
remote_addrs = reject_trusted_ip_addresses(remote_addrs)
- return remote_addrs.first if remote_addrs.any?
+ if remote_addrs.any?
+ remote_addrs.first
+ else
+ forwarded_ips = self.forwarded_for
- forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))
- .map { |ip| strip_port(ip) }
-
- return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
+ reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
+ end
end
# The media type (type/subtype) portion of the CONTENT_TYPE header
# without any media type parameters. e.g., when CONTENT_TYPE is
# "text/plain;charset=utf-8", the media-type is "text/plain".
@@ -321,10 +400,11 @@
# A request body is also assumed to contain form-data when no
# Content-Type header is provided and the request_method is POST.
def form_data?
type = media_type
meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD)
+
(meth == POST && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
end
# Determine whether the request body contains data by checking
# the request media_type against registered parse-data media-types
@@ -375,12 +455,10 @@
# The union of GET and POST data.
#
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
def params
self.GET.merge(self.POST)
- rescue EOFError
- self.GET.dup
end
# Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
#
# The parameter is updated wherever it was previous defined, so GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
@@ -410,13 +488,11 @@
post_value, get_value = self.POST.delete(k), self.GET.delete(k)
post_value || get_value
end
def base_url
- url = "#{scheme}://#{host}"
- url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme]
- url
+ "#{scheme}://#{host_with_port}"
end
# Tries to return a remake of the original request URL as a string.
def url
base_url + fullpath
@@ -469,10 +545,24 @@
private
def default_session; {}; end
+ # Assist with compatibility when processing `X-Forwarded-For`.
+ def wrap_ipv6(host)
+ # Even thought IPv6 addresses should be wrapped in square brackets,
+ # sometimes this is not done in various legacy/underspecified headers.
+ # So we try to fix this situation for compatibility reasons.
+
+ # Try to detect IPv6 addresses which aren't escaped yet:
+ if !host.start_with?('[') && host.count(':') > 1
+ "[#{host}]"
+ else
+ host
+ end
+ end
+
def parse_http_accept_header(header)
header.to_s.split(/\s*,\s*/).map do |part|
attribute, parameters = part.split(/\s*;\s*/, 2)
quality = 1.0
if parameters and /\Aq=([\d.]+)/ =~ parameters
@@ -492,31 +582,43 @@
def parse_multipart
Rack::Multipart.extract_multipart(self, query_parser)
end
- def split_ip_addresses(ip_addresses)
- ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
+ def split_header(value)
+ value ? value.strip.split(/[,\s]+/) : []
end
- def strip_port(ip_address)
- # IPv6 format with optional port: "[2001:db8:cafe::17]:47011"
- # returns: "2001:db8:cafe::17"
- sep_start = ip_address.index('[')
- sep_end = ip_address.index(']')
- if (sep_start && sep_end)
- return ip_address[sep_start + 1, sep_end - 1]
- end
+ AUTHORITY = /
+ # The host:
+ (?<host>
+ # An IPv6 address:
+ (\[(?<ip6>.*)\])
+ |
+ # An IPv4 address:
+ (?<ip4>[\d\.]+)
+ |
+ # A hostname:
+ (?<name>[a-zA-Z0-9\.\-]+)
+ )
+ # The optional port:
+ (:(?<port>\d+))?
+ /x
- # IPv4 format with optional port: "192.0.2.43:47011"
- # returns: "192.0.2.43"
- sep = ip_address.index(':')
- if (sep && ip_address.count(':') == 1)
- return ip_address[0, sep]
+ private_constant :AUTHORITY
+
+ def split_authority(authority)
+ if match = AUTHORITY.match(authority)
+ if address = match[:ip6]
+ return match[:host], address, match[:port]&.to_i
+ else
+ return match[:host], match[:host], match[:port]&.to_i
+ end
end
- ip_address
+ # Give up!
+ return authority, authority, nil
end
def reject_trusted_ip_addresses(ip_addresses)
ip_addresses.reject { |ip| trusted_proxy?(ip) }
end
@@ -535,15 +637,9 @@
if (comma_index = header.index(','))
header[0, comma_index]
else
header
end
- end
- end
-
- def extract_port(uri)
- if (colon_index = uri.index(':'))
- uri[colon_index + 1, uri.length]
end
end
end
include Env