# frozen_string_literal: true require_relative 'constants' require_relative 'utils' require_relative 'media_type' 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 class << self attr_accessor :ip_filter # The priority when checking forwarded headers. The default # is [:forwarded, :x_forwarded], which means, check the # +Forwarded+ header first, followed by the appropriate # X-Forwarded-* header. You can revert the priority by # reversing the priority, or remove checking of either # or both headers by removing elements from the array. # # This should be set as appropriate in your environment # based on what reverse proxies are in use. If you are not # using reverse proxies, you should probably use an empty # array. attr_accessor :forwarded_priority # The priority when checking either the X-Forwarded-Proto # or X-Forwarded-Scheme header for the forwarded protocol. # The default is [:proto, :scheme], to try the # X-Forwarded-Proto header before the # X-Forwarded-Scheme header. Rack 2 had behavior # similar to [:scheme, :proto]. You can remove either or # both of the entries in array to ignore that respective header. attr_accessor :x_forwarded_proto_priority end @forwarded_priority = [:forwarded, :x_forwarded] @x_forwarded_proto_priority = [:proto, :scheme] valid_ipv4_octet = /\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])/ trusted_proxies = Regexp.union( /\A127#{valid_ipv4_octet}{3}\z/, # localhost IPv4 range 127.x.x.x, per RFC-3330 /\A::1\z/, # localhost IPv6 ::1 /\Af[cd][0-9a-f]{2}(?::[0-9a-f]{0,4}){0,7}\z/i, # private IPv6 range fc00 .. fdff /\A10#{valid_ipv4_octet}{3}\z/, # private IPv4 range 10.x.x.x /\A172\.(1[6-9]|2[0-9]|3[01])#{valid_ipv4_octet}{2}\z/, # private IPv4 range 172.16.0.0 .. 172.31.255.255 /\A192\.168#{valid_ipv4_octet}{2}\z/, # private IPv4 range 192.168.x.x /\Alocalhost\z|\Aunix(\z|:)/i, # localhost hostname, and unix domain sockets ) self.ip_filter = lambda { |ip| trusted_proxies.match?(ip) } ALLOWED_SCHEMES = %w(https http wss ws).freeze def initialize(env) @env = env @params = nil end def params @params ||= super end def update_param(k, v) super @params = nil end def delete_param(k) v = super @params = nil v end module Env # The environment of the request. attr_reader :env def initialize(env) @env = env # This module is included at least in `ActionDispatch::Request` # The call to `super()` allows additional mixed-in initializers are called super() end # Predicate method to test to see if a header of the given +name+ exists. def has_header?(name) @env.key? env_key(name) end # Get the value from the request environment with the specified +name+, returning +nil+ if it doesn't exist. # # When invoked with a lower case (canonical) header name, it will be # access the HTTP header of that name from the request environment. # # request.get_header('accept') # => '*/*' # # When invoked with a name which contains periods +.+ (e.g. 'rack.input'), # it willm directly read from the request environment. This is considered # legacy behaviour and you should use env directly. # # # Legacy usage: # request.get_header('rack.input') # # # Use env directly: # request.env['rack.input'] # # Note that +rack.input+ is also valid HTTP header name and therefore at # this time it's impossible to use headers that contain periods. # # When invoked with a name which matches a CGI variable (as defined in # RFC3875), it will be directly read from the request environment. This # is considered legacy behaviour and you should access env directly. # # # Legacy usage: # request.get_header('PATH_INFO') # # # Use env directly: # request.env['PATH_INFO'] # def get_header(name) @env[env_key(name)] end # Fetch an HTTP header using using +Hash#fetch+. Yields the internal CGI # variable key which you should use if you want to set the default # value. See get_header details on how name lookup works. def fetch_header(name, &block) @env.fetch(env_key(name), &block) end # Loops through each key / value pair in the request HTTP headers. Yields # header names in their canonical form. # # request.each do |key, value| # [key, value] # => ["accept", "*/*"] # end # # Note that CGI style headers cannot represent all possible HTTP headers # and thus the reconstruction of HTTP headers is intriniscally lossy. # The actual behaviour will depend on your server configuration but # generally, +_+ characters will be mapped to +-+ characters. Incoming # headers with underscores (+_+) will thus be indistinguisable from those # with dashes (+-+). def each_header(&block) @env.each do |key, value| if name = header_name(key) yield name, value end end end # Set a request HTTP header value for +name+ to +value+ overwriting any # existing value. def set_header(name, value) @env[env_key(name)] = value end # Add a request HTTP header that may have multiple values. # # request.add_header 'accept', 'image/png' # request.add_header 'accept', '*/*' # # request.get_header('accept') # # => 'image/png,*/*' # # See get_header for details on name mapping. def add_header(name, value) key = env_key(name) if value.nil? @env[key] elsif current = env[key] if current.empty? @env[key] = value else @env[key] = "#{current},#{value}" end else @env[key] = value end end # Delete a request HTTP header with the specified +name+. See get_header # for details on name mapping. def delete_header(name) @env.delete(env_key(name)) end def initialize_copy(other) @env = other.env.dup end private # These are CGI variables as defined by RFC3875. # https://www.rfc-editor.org/rfc/rfc3875.html#section-4.1 # We explicitly use these to detect cases where someone # uses get_header or set_header with one of the named # variables. CGI_VARIABLES = Set.new([ "AUTH_TYPE", "CONTENT_LENGTH", "CONTENT_TYPE", "GATEWAY_INTERFACE", "PATH_INFO", "PATH_TRANSLATED", "QUERY_STRING", "REMOTE_ADDR", "REMOTE_HOST", "REMOTE_IDENT", "REMOTE_USER", "REQUEST_METHOD", "SCRIPT_NAME", "SERVER_NAME", "SERVER_PORT", "SERVER_PROTOCOL", "SERVER_SOFTWARE" ]).freeze # This is the pattern for +token+ strings as defined by RFC7231 with extra # limitations that the headers must be lower case (canonical form) and # don't have a period character. # https://tools.ietf.org/html/rfc7231#appendix-C HTTP_HEADER_PATTERN = /\A[!#$%&'*+\-^_`|~0-9a-z]+\z/ # Converts an HTTP header name to an environment variable name if it is # not contained within the headers hash. Considers several cases. # # 1. The name matches a known CGI variable e.g. +SERVER_NAME+: the name is # used directly. This is considered a legacy usage and a warning will # be issued. # 2. The name matches a canonical header (lower case) without any periods # e.g. +accept+: This is considered normal usage. # 3. The name isn't a CGI variable or a canonical header e.g. # +rack.input+: The name is used directly. This is considered legacy # usage and a warning will be issued. # # To avoid warnings.only use valid canoincal header names. def env_key(name) key = name.to_s if CGI_VARIABLES.include?(key) warn "Using CGI variable keys (#{key}) with header methods is deprecated and will be removed in Rack 3.1! Please use env directly.", uplevel: 2 elsif HTTP_HEADER_PATTERN.match?(key) key = key.upcase key.tr!('-', '_') key.prepend('HTTP_') else warn "Using env keys (#{key}) with header methods is deprecated and will be removed in Rack 3.1! Please use env directly.", uplevel: 2 end return key end # Matches CGI variables which represent HTTP headers. HEADER_KEY_PATTERN = /\AHTTP_(.+)\z/ # Map a CGI variable which represents an HTTP header into a canonical HTTP # header name. Since the process of converting HTTP headers into CGI # variables is lossy, we do best effort reconstruction. # # 1. Converting +_+ characters to +-+ characters. # 2. Forcing lower case canonical form. def header_name(key) if match = HEADER_KEY_PATTERN.match(key) name = match[1] name.tr!('_', '-') name.downcase! return name end end end module Helpers # The set of form-data media-types. Requests that do not indicate # one of the media types present in this list will not be eligible # for form-data / param parsing. FORM_DATA_MEDIA_TYPES = [ 'application/x-www-form-urlencoded', 'multipart/form-data' ] # The set of media-types. Requests that do not indicate # one of the media types present in this list will not be eligible # for param parsing like soap attachments or generic multiparts PARSEABLE_DATA_MEDIA_TYPES = [ 'multipart/related', 'multipart/mixed' ] # 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' HTTP_FORWARDED = 'HTTP_FORWARDED' # The value of the scheme sent to the proxy. HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME' # 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 specifying https scheme was used. HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL' def body env[RACK_INPUT] end def script_name env[SCRIPT_NAME].to_s end def script_name=(value) env[SCRIPT_NAME] = value.to_s end def path_info env[PATH_INFO].to_s end def path_info=(value) env[PATH_INFO] = value.to_s end def request_method env[REQUEST_METHOD] end def query_string env[QUERY_STRING].to_s end def content_length env['CONTENT_LENGTH'] end def logger env[RACK_LOGGER] end def user_agent env['HTTP_USER_AGENT'] end # the referer of the client def referer env['HTTP_REFERER'] end alias referrer referer def session env.fetch(RACK_SESSION) do |key| env[key] = default_session end end def session_options env.fetch(RACK_SESSION_OPTIONS) do |key| env[key] = {} end end # Checks the HTTP request method (or verb) to see if it was of type DELETE def delete?; request_method == DELETE end # Checks the HTTP request method (or verb) to see if it was of type GET def get?; request_method == GET end # Checks the HTTP request method (or verb) to see if it was of type HEAD def head?; request_method == HEAD end # Checks the HTTP request method (or verb) to see if it was of type OPTIONS def options?; request_method == OPTIONS end # Checks the HTTP request method (or verb) to see if it was of type LINK def link?; request_method == LINK end # Checks the HTTP request method (or verb) to see if it was of type PATCH def patch?; request_method == PATCH end # Checks the HTTP request method (or verb) to see if it was of type POST def post?; request_method == POST end # Checks the HTTP request method (or verb) to see if it was of type PUT def put?; request_method == PUT end # Checks the HTTP request method (or verb) to see if it was of type TRACE def trace?; request_method == TRACE end # Checks the HTTP request method (or verb) to see if it was of type UNLINK def unlink?; request_method == UNLINK end def scheme if env['HTTPS'] == 'on' 'https' elsif env['HTTP_X_FORWARDED_SSL'] == 'on' 'https' elsif forwarded_scheme forwarded_scheme else env[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 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 env[SERVER_NAME] end def server_port if port = env[SERVER_PORT] Integer(port) end end def cookies env = self.env hash = env.fetch(RACK_REQUEST_COOKIE_HASH) do |key| env[key] = {} end string = env[HTTP_COOKIE] unless string == env[RACK_REQUEST_COOKIE_STRING] hash.replace Utils.parse_cookies_header(string) env[RACK_REQUEST_COOKIE_STRING] = string end hash end def content_type content_type = env['CONTENT_TYPE'] content_type.nil? || content_type.empty? ? nil : content_type end def xhr? get_header("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest" end # The `HTTP_HOST` header. def host_authority env[HTTP_HOST] end def host_with_port(authority = self.authority) host, _, port = split_authority(authority) if port == DEFAULT_PORTS[self.scheme] host else authority end end # Returns a formatted host, suitable for being used in a URI. def host 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 authority = self.authority _, _, port = split_authority(authority) if port return port end end if forwarded_port = self.forwarded_port return forwarded_port.last end if scheme = self.scheme if port = DEFAULT_PORTS[scheme] return port end end self.server_port end def forwarded_for forwarded_priority.each do |type| case type when :forwarded if forwarded_for = get_http_forwarded(:for) return(forwarded_for.map! do |authority| split_authority(authority)[1] end) end when :x_forwarded if value = env[HTTP_X_FORWARDED_FOR] return(split_header(value).map do |authority| split_authority(wrap_ipv6(authority))[1] end) end end end nil end def forwarded_port forwarded_priority.each do |type| case type when :forwarded if forwarded = get_http_forwarded(:for) return(forwarded.map do |authority| split_authority(authority)[2] end.compact) end when :x_forwarded if value = env[HTTP_X_FORWARDED_PORT] return split_header(value).map(&:to_i) end end end nil end def forwarded_authority forwarded_priority.each do |type| case type when :forwarded if forwarded = get_http_forwarded(:host) return forwarded.last end when :x_forwarded if value = env[HTTP_X_FORWARDED_HOST] return wrap_ipv6(split_header(value).last) end end end nil end def ssl? scheme == 'https' || scheme == 'wss' end def ip remote_addresses = split_header(env['REMOTE_ADDR']) external_addresses = reject_trusted_ip_addresses(remote_addresses) unless external_addresses.empty? return external_addresses.last end if forwarded_for = self.forwarded_for unless forwarded_for.empty? # The forwarded for addresses are ordered: client, proxy1, proxy2. # So we reject all the trusted addresses (proxy*) and return the # last client. Or if we trust everyone, we just return the first # address. return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first end end # If all the addresses are trusted, and we aren't forwarded, just return # the first remote address, which represents the source of the request. remote_addresses.first 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". # # For more information on the use of media types in HTTP, see: # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 def media_type MediaType.type(content_type) end # The media type parameters provided in CONTENT_TYPE as a Hash, or # an empty Hash if no CONTENT_TYPE or media-type parameters were # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8", # this method responds with the following Hash: # { 'charset' => 'utf-8' } def media_type_params MediaType.params(content_type) end # The character set of the request body if a "charset" media type # parameter was given, or nil if no "charset" was specified. Note # that, per RFC2616, text/* media types that specify no explicit # charset are to be considered ISO-8859-1. def content_charset media_type_params['charset'] end # Determine whether the request body contains form-data by checking # the request content-type for one of the media-types: # "application/x-www-form-urlencoded" or "multipart/form-data". The # list of form-data media types can be modified through the # +FORM_DATA_MEDIA_TYPES+ array. # # 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 = env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] || env[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 def parseable_data? PARSEABLE_DATA_MEDIA_TYPES.include?(media_type) end # Returns the data received in the query string. def GET env = self.env if env[RACK_REQUEST_QUERY_STRING] == query_string env[RACK_REQUEST_QUERY_HASH] else query_hash = parse_query(query_string, '&') env[RACK_REQUEST_QUERY_STRING] = query_string env[RACK_REQUEST_QUERY_HASH] = query_hash end end # Returns the data received in the request body. # # This method support both application/x-www-form-urlencoded and # multipart/form-data. def POST env = self.env if env[RACK_INPUT].nil? raise "Missing rack.input" elsif env[RACK_REQUEST_FORM_INPUT] == env[RACK_INPUT] env[RACK_REQUEST_FORM_INPUT] elsif form_data? || parseable_data? unless (env[RACK_REQUEST_FORM_HASH] = parse_multipart) form_vars = env[RACK_INPUT].read # Fix for Safari Ajax postings that always append \0 # form_vars.sub!(/\0\z/, '') # performance replacement: form_vars.slice!(-1) if form_vars.end_with?("\0") env[RACK_REQUEST_FORM_VARS] = form_vars env[RACK_REQUEST_FORM_HASH] = parse_query(form_vars, '&') end env[RACK_REQUEST_FORM_INPUT] = env[RACK_INPUT] else env[RACK_REQUEST_FORM_INPUT] = env[RACK_INPUT] env[RACK_REQUEST_FORM_HASH] = {} end return env[RACK_REQUEST_FORM_HASH] end # 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) 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. # # env['rack.input'] is not touched. def update_param(k, v) found = false if self.GET.has_key?(k) found = true self.GET[k] = v end if self.POST.has_key?(k) found = true self.POST[k] = v end unless found self.GET[k] = v end end # Destructively delete a parameter, whether it's in GET or POST. Returns the value of the deleted parameter. # # If the parameter is in both GET and POST, the POST value takes precedence since that's how #params works. # # env['rack.input'] is not touched. def delete_param(k) post_value, get_value = self.POST.delete(k), self.GET.delete(k) post_value || get_value end def base_url "#{scheme}://#{host_with_port}" end # Tries to return a remake of the original request URL as a string. def url base_url + fullpath end def path script_name + path_info end def fullpath query_string.empty? ? path : "#{path}?#{query_string}" end def accept_encoding parse_http_accept_header(env["HTTP_ACCEPT_ENCODING"]) end def accept_language parse_http_accept_header(env["HTTP_ACCEPT_LANGUAGE"]) end def trusted_proxy?(ip) Rack::Request.ip_filter.call(ip) end # shortcut for request.params[key] def [](key) warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1) params[key.to_s] end # shortcut for request.params[key] = value # # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params. def []=(key, value) warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1) params[key.to_s] = value end # like Hash#values_at def values_at(*keys) keys.map { |key| params[key] } end 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 quality = $1.to_f end [attribute, quality] end end # Get an array of values set in the RFC 7239 `Forwarded` request header. def get_http_forwarded(token) Utils.forwarded_values(env[HTTP_FORWARDED])&.[](token) end def query_parser Utils.default_query_parser end def parse_query(qs, d = '&') query_parser.parse_nested_query(qs, d) end def parse_multipart Rack::Multipart.extract_multipart(self, query_parser) end def split_header(value) value ? value.strip.split(/[,\s]+/) : [] end # ipv6 extracted from resolv stdlib, simplified # to remove numbered match group creation. ipv6 = Regexp.union( /(?:[0-9A-Fa-f]{1,4}:){7} [0-9A-Fa-f]{1,4}/x, /(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? :: (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?/x, /(?:[0-9A-Fa-f]{1,4}:){6,6} \d+\.\d+\.\d+\.\d+/x, /(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? :: (?:[0-9A-Fa-f]{1,4}:)* \d+\.\d+\.\d+\.\d+/x, /[Ff][Ee]80 (?::[0-9A-Fa-f]{1,4}){7} %[-0-9A-Za-z._~]+/x, /[Ff][Ee]80: (?: (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? :: (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? | :(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? )? :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+/x) AUTHORITY = / \A (? # Match IPv6 as a string of hex digits and colons in square brackets \[(?
#{ipv6})\] | # Match any other printable string (except square brackets) as a hostname (?
[[[:graph:]&&[^\[\]]]]*?) ) (:(?\d+))? \z /x private_constant :AUTHORITY def split_authority(authority) return [] if authority.nil? return [] unless match = AUTHORITY.match(authority) return match[:host], match[:address], match[:port]&.to_i end def reject_trusted_ip_addresses(ip_addresses) ip_addresses.reject { |ip| trusted_proxy?(ip) } end FORWARDED_SCHEME_KEYS = { proto: HTTP_X_FORWARDED_PROTO, scheme: HTTP_X_FORWARDED_SCHEME }.freeze private_constant :FORWARDED_SCHEME_KEYS def forwarded_scheme forwarded_priority.each do |type| case type when :forwarded if (forwarded_proto = get_http_forwarded(:proto)) && (scheme = allowed_scheme(forwarded_proto.last)) return scheme end when :x_forwarded x_forwarded_proto_priority.each do |x_type| if header = FORWARDED_SCHEME_KEYS[x_type] split_header(env[header]).reverse_each do |scheme| if allowed_scheme(scheme) return scheme end end end end end end nil end def allowed_scheme(header) header if ALLOWED_SCHEMES.include?(header) end def extract_proto_header(header) if header if (comma_index = header.index(',')) header[0, comma_index] else header end end end def forwarded_priority Request.forwarded_priority end def x_forwarded_proto_priority Request.x_forwarded_proto_priority end end include Env include Helpers end end require_relative 'multipart' unless defined?(Rack::Multipart)