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