lib/rack/utils.rb in rack-2.1.4.4 vs lib/rack/utils.rb in rack-2.2.0

- old
+ new

@@ -3,21 +3,20 @@ require 'uri' require 'fileutils' require 'set' require 'tempfile' -require 'rack/query_parser' require 'time' -require_relative 'core_ext/regexp' +require_relative 'query_parser' module Rack # Rack::Utils contains a grab-bag of useful methods for writing web # applications adopted from all kinds of Ruby libraries. module Utils - using ::Rack::RegexpExtensions + (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4' ParameterTypeError = QueryParser::ParameterTypeError InvalidParameterError = QueryParser::InvalidParameterError DEFAULT_SEP = QueryParser::DEFAULT_SEP COMMON_SEP = QueryParser::COMMON_SEP @@ -28,58 +27,44 @@ end # The default number of bytes to allow parameter keys to take up. # This helps prevent a rogue client from flooding a Request. self.default_query_parser = QueryParser.make_default(65536, 100) + module_function + # URI escapes. (CGI style space to +) def escape(s) URI.encode_www_form_component(s) end - module_function :escape # Like URI escaping, but with %20 instead of +. Strictly speaking this is # true URI escaping. def escape_path(s) ::URI::DEFAULT_PARSER.escape s end - module_function :escape_path # Unescapes the **path** component of a URI. See Rack::Utils.unescape for # unescaping query parameters or form components. def unescape_path(s) ::URI::DEFAULT_PARSER.unescape s end - module_function :unescape_path - # Unescapes a URI escaped string with +encoding+. +encoding+ will be the # target encoding of the string returned, and it defaults to UTF-8 def unescape(s, encoding = Encoding::UTF_8) URI.decode_www_form_component(s, encoding) end - module_function :unescape class << self - attr_accessor :multipart_total_part_limit - - attr_accessor :multipart_file_limit - - # multipart_part_limit is the original name of multipart_file_limit, but - # the limit only counts parts with filenames. - alias multipart_part_limit multipart_file_limit - alias multipart_part_limit= multipart_file_limit= + attr_accessor :multipart_part_limit end - # The maximum number of file parts a request can contain. Accepting too - # many parts can lead to the server running out of file handles. + # The maximum number of parts a request can contain. Accepting too many part + # can lead to the server running out of file handles. # Set to `0` for no limit. - self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i + self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i - # The maximum total number of parts a request can contain. Accepting too - # many can lead to excessive memory use and parsing time. - self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i - def self.param_depth_limit default_query_parser.param_depth_limit end def self.param_depth_limit=(v) @@ -97,36 +82,34 @@ if defined?(Process::CLOCK_MONOTONIC) def clock_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end else + # :nocov: def clock_time Time.now.to_f end + # :nocov: end - module_function :clock_time def parse_query(qs, d = nil, &unescaper) Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper) end - module_function :parse_query def parse_nested_query(qs, d = nil) Rack::Utils.default_query_parser.parse_nested_query(qs, d) end - module_function :parse_nested_query def build_query(params) params.map { |k, v| if v.class == Array build_query(v.map { |x| [k, x] }) else v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}" end }.join("&") end - module_function :build_query def build_nested_query(value, prefix = nil) case value when Array value.map { |v| @@ -141,24 +124,26 @@ else raise ArgumentError, "value must be a Hash" if prefix.nil? "#{prefix}=#{escape(value)}" end end - module_function :build_nested_query def q_values(q_value_header) - q_value_header.to_s.split(',').map do |part| - value, parameters = part.split(';', 2).map(&:strip) + q_value_header.to_s.split(/\s*,\s*/).map do |part| + value, parameters = part.split(/\s*;\s*/, 2) quality = 1.0 if parameters && (md = /\Aq=([\d.]+)/.match(parameters)) quality = md[1].to_f end [value, quality] end end - module_function :q_values + # Return best accept value to use, based on the algorithm + # in RFC 2616 Section 14. If there are multiple best + # matches (same specificity and quality), the value returned + # is arbitrary. def best_q_match(q_value_header, available_mimes) values = q_values(q_value_header) matches = values.map do |req_mime, quality| match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) } @@ -167,11 +152,10 @@ end.compact.sort_by do |match, quality| (match.split('/', 2).count('*') * -10) + quality end.last matches && matches.first end - module_function :best_q_match ESCAPE_HTML = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", @@ -184,26 +168,31 @@ # Escape ampersands, brackets and quotes to their HTML/XML entities. def escape_html(string) string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] } end - module_function :escape_html def select_best_encoding(available_encodings, accept_encoding) # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - expanded_accept_encoding = - accept_encoding.each_with_object([]) do |(m, q), list| - if m == "*" - (available_encodings - accept_encoding.map(&:first)) - .each { |m2| list << [m2, q] } - else - list << [m, q] + expanded_accept_encoding = [] + + accept_encoding.each do |m, q| + preference = available_encodings.index(m) || available_encodings.size + + if m == "*" + (available_encodings - accept_encoding.map(&:first)).each do |m2| + expanded_accept_encoding << [m2, q, preference] end + else + expanded_accept_encoding << [m, q, preference] end + end - encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map!(&:first) + encoding_candidates = expanded_accept_encoding + .sort_by { |_, q, p| [-q, p] } + .map!(&:first) unless encoding_candidates.include?("identity") encoding_candidates.push("identity") end @@ -211,31 +200,23 @@ encoding_candidates.delete(m) if q == 0.0 end (encoding_candidates & available_encodings)[0] end - module_function :select_best_encoding def parse_cookies(env) parse_cookies_header env[HTTP_COOKIE] end - module_function :parse_cookies def parse_cookies_header(header) - # According to RFC 2109: - # If multiple cookies satisfy the criteria above, they are ordered in - # the Cookie header such that those with more specific Path attributes - # precede those with less specific. Ordering with respect to other - # attributes (e.g., Domain) is unspecified. - return {} unless header - header.split(/[;,] */n).each_with_object({}) do |cookie, cookies| - next if cookie.empty? - key, value = cookie.split('=', 2) - cookies[key] = (unescape(value) rescue value) unless cookies.key?(key) - end + # According to RFC 6265: + # The syntax for cookie headers only supports semicolons + # User Agent -> Server == + # Cookie: SID=31d4d96e407aad42; lang=en-US + cookies = parse_query(header, ';') { |s| unescape(s) rescue s } + cookies.each_with_object({}) { |(k, v), hash| hash[k] = Array === v ? v.first : v } end - module_function :parse_cookies_header def add_cookie_to_header(header, key, value) case value when Hash domain = "; domain=#{value[:domain]}" if value[:domain] @@ -273,17 +254,15 @@ (header + [cookie]).join("\n") else raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}" end end - module_function :add_cookie_to_header def set_cookie_header!(header, key, value) header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value) nil end - module_function :set_cookie_header! def make_delete_cookie_header(header, key, value) case header when nil, '' cookies = [] @@ -291,29 +270,34 @@ cookies = header.split("\n") when Array cookies = header end - regexp = if value[:domain] - /\A#{escape(key)}=.*domain=#{value[:domain]}/ - elsif value[:path] - /\A#{escape(key)}=.*path=#{value[:path]}/ + key = escape(key) + domain = value[:domain] + path = value[:path] + regexp = if domain + if path + /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/ + else + /\A#{key}=.*domain=#{domain}(?:;|$)/ + end + elsif path + /\A#{key}=.*path=#{path}(?:;|$)/ else - /\A#{escape(key)}=/ + /\A#{key}=/ end cookies.reject! { |cookie| regexp.match? cookie } cookies.join("\n") end - module_function :make_delete_cookie_header def delete_cookie_header!(header, key, value = {}) header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value) nil end - module_function :delete_cookie_header! # Adds a cookie that will *remove* a cookie from the client. Hence the # strange method name. def add_remove_cookie_to_header(header, key, value = {}) new_header = make_delete_cookie_header(header, key, value) @@ -322,16 +306,14 @@ { value: '', path: nil, domain: nil, max_age: '0', expires: Time.at(0) }.merge(value)) end - module_function :add_remove_cookie_to_header def rfc2822(time) time.rfc2822 end - module_function :rfc2822 # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead # of '% %b %Y'. # It assumes that the time is in GMT to comply to the RFC 2109. # @@ -343,38 +325,35 @@ def rfc2109(time) wday = Time::RFC2822_DAY_NAME[time.wday] mon = Time::RFC2822_MONTH_NAME[time.mon - 1] time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT") end - module_function :rfc2109 # Parses the "Range:" header, if present, into an array of Range objects. # Returns nil if the header is missing or syntactically invalid. # Returns an empty array if none of the ranges are satisfiable. def byte_ranges(env, size) warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE get_byte_ranges env['HTTP_RANGE'], size end - module_function :byte_ranges def get_byte_ranges(http_range, size) # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35> return nil unless http_range && http_range =~ /bytes=([^;]+)/ ranges = [] $1.split(/,\s*/).each do |range_spec| - return nil unless range_spec.include?('-') - range = range_spec.split('-') - r0, r1 = range[0], range[1] - if r0.nil? || r0.empty? - return nil if r1.nil? + return nil unless range_spec =~ /(\d*)-(\d*)/ + r0, r1 = $1, $2 + if r0.empty? + return nil if r1.empty? # suffix-byte-range-spec, represents trailing suffix of file r0 = size - r1.to_i r0 = 0 if r0 < 0 r1 = size - 1 else r0 = r0.to_i - if r1.nil? + if r1.empty? r1 = size - 1 else r1 = r1.to_i return nil if r1 < r0 # backwards range is syntactically invalid r1 = size - 1 if r1 >= size @@ -382,11 +361,10 @@ end ranges << (r0..r1) if r0 <= r1 end ranges end - module_function :get_byte_ranges # Constant time string comparison. # # NOTE: the values compared should be of fixed length, such as strings # that have already been processed by HMAC. This should not be used @@ -399,11 +377,10 @@ r, i = 0, -1 b.each_byte { |v| r |= v ^ l[i += 1] } r == 0 end - module_function :secure_compare # Context allows the use of a compatible middleware at different points # in a request handling stack. A compatible middleware must define # #context which should take the arguments env and app. The first of which # would be the request environment. The second of which would be the rack @@ -432,10 +409,18 @@ # A case-insensitive Hash that preserves the original case of a # header when set. # # @api private class HeaderHash < Hash # :nodoc: + def self.[](headers) + if headers.is_a?(HeaderHash) && !headers.frozen? + return headers + else + return self.new(headers) + end + end + def initialize(hash = {}) super() @names = {} hash.each { |k, v| self[k] = v } end @@ -444,10 +429,16 @@ def initialize_copy(other) super @names = other.names.dup end + # on clear, we need to clear @names hash + def clear + super + @names.clear + end + def each super do |k, v| yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v) end end @@ -588,11 +579,10 @@ SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" } else status.to_i end end - module_function :status_code PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact) def clean_path_info(path_info) parts = path_info.split PATH_SEPS @@ -602,20 +592,18 @@ parts.each do |part| next if part.empty? || part == '.' part == '..' ? clean.pop : clean << part end - clean.unshift '/' if parts.empty? || parts.first.empty? - - ::File.join clean + clean_path = clean.join(::File::SEPARATOR) + clean_path.prepend("/") if parts.empty? || parts.first.empty? + clean_path end - module_function :clean_path_info NULL_BYTE = "\0" def valid_path?(path) path.valid_encoding? && !path.include?(NULL_BYTE) end - module_function :valid_path? end end