module Rack::Utils

Rack::Utils contains a grab-bag of useful methods for writing web applications adopted from all kinds of Ruby libraries.

Constants

COMMON_SEP
DEFAULT_SEP
ESCAPE_HTML
ESCAPE_HTML_PATTERN
HTTP_STATUS_CODES

Every standard HTTP code mapped to the appropriate message. Generated with:

curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
  ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
            puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
InvalidParameterError
KeySpaceConstrainedParams
NULL_BYTE
PATH_SEPS
ParameterTypeError
ParamsTooDeepError
STATUS_WITH_NO_ENTITY_BODY

Responses with HTTP status codes that should not have an entity body

SYMBOL_TO_STATUS_CODE

Attributes

Public Class Methods

# File lib/rack/utils.rb, line 155
def forwarded_values(forwarded_header)
  return nil unless forwarded_header
  forwarded_header = forwarded_header.to_s.gsub("\n", ";")

  forwarded_header.split(';').each_with_object({}) do |field, values|
    field.split(',').each do |pair|
      pair = pair.split('=').map(&:strip).join('=')
      return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
      (values[$1.downcase.to_sym] ||= []) << $2
    end
  end
end
# File lib/rack/utils.rb, line 87
def self.key_space_limit
  warn("`Rack::Utils.key_space_limit` is deprecated as this value no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
  65536
end
# File lib/rack/utils.rb, line 92
def self.key_space_limit=(v)
  warn("`Rack::Utils.key_space_limit=` is deprecated and no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
end
# File lib/rack/utils.rb, line 79
def self.param_depth_limit
  default_query_parser.param_depth_limit
end
# File lib/rack/utils.rb, line 83
def self.param_depth_limit=(v)
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
end

Public Instance Methods

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.

# File lib/rack/utils.rb, line 173
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) }
    next unless match
    [match, quality]
  end.compact.sort_by do |match, quality|
    (match.split('/', 2).count('*') * -10) + quality
  end.last
  matches&.first
end
# File lib/rack/utils.rb, line 126
def build_nested_query(value, prefix = nil)
  case value
  when Array
    value.map { |v|
      build_nested_query(v, "#{prefix}[]")
    }.join("&")
  when Hash
    value.map { |k, v|
      build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
    }.delete_if(&:empty?).join('&')
  when nil
    escape(prefix)
  else
    raise ArgumentError, "value must be a Hash" if prefix.nil?
    "#{escape(prefix)}=#{escape(value)}"
  end
end
# File lib/rack/utils.rb, line 116
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

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.

# File lib/rack/utils.rb, line 431
def byte_ranges(env, size)
  get_byte_ranges env['HTTP_RANGE'], size
end
# File lib/rack/utils.rb, line 635
def clean_path_info(path_info)
  parts = path_info.split PATH_SEPS

  clean = []

  parts.each do |part|
    next if part.empty? || part == '.'
    part == '..' ? clean.pop : clean << part
  end

  clean_path = clean.join(::File::SEPARATOR)
  clean_path.prepend("/") if parts.empty? || parts.first.empty?
  clean_path
end
# File lib/rack/utils.rb, line 97
def clock_time
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
end

URI escapes. (CGI style space to +)

# File lib/rack/utils.rb, line 37
def escape(s)
  URI.encode_www_form_component(s)
end

Escape ampersands, brackets and quotes to their HTML/XML entities.

# File lib/rack/utils.rb, line 198
def escape_html(string)
  string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
end

Like URI escaping, but with %20 instead of +. Strictly speaking this is true URI escaping.

# File lib/rack/utils.rb, line 43
def escape_path(s)
  ::URI::DEFAULT_PARSER.escape s
end
# File lib/rack/utils.rb, line 435
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?
      # 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?
        r1 = size - 1
      else
        r1 = r1.to_i
        return nil  if r1 < r0  # backwards range is syntactically invalid
        r1 = size - 1  if r1 >= size
      end
    end
    ranges << (r0..r1)  if r0 <= r1
  end

  return [] if ranges.map(&:size).sum > size

  ranges
end

Parse cookies from the provided request environment using parse_cookies_header. Returns a map of cookie key to cookie value.

parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
# => {'myname' => 'myvalue'}
# File lib/rack/utils.rb, line 278
def parse_cookies(env)
  parse_cookies_header env[HTTP_COOKIE]
end

Parse cookies from the provided header value according to RFC6265. The syntax for cookie headers only supports semicolons. Returns a map of cookie key to cookie value.

parse_cookies_header('myname=myvalue; max-age=0')
# => {"myname"=>"myvalue", "max-age"=>"0"}
# File lib/rack/utils.rb, line 244
def parse_cookies_header(value)
  return {} unless value

  value.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
end
# File lib/rack/utils.rb, line 112
def parse_nested_query(qs, d = nil)
  Rack::Utils.default_query_parser.parse_nested_query(qs, d)
end
# File lib/rack/utils.rb, line 108
def parse_query(qs, d = nil, &unescaper)
  Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
end
# File lib/rack/utils.rb, line 144
def q_values(q_value_header)
  q_value_header.to_s.split(',').map do |part|
    value, parameters = part.split(';', 2).map(&:strip)
    quality = 1.0
    if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
      quality = md[1].to_f
    end
    [value, quality]
  end
end
# File lib/rack/utils.rb, line 424
def rfc2822(time)
  time.rfc2822
end

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 on variable length plaintext strings because it could leak length info via timing attacks.

# File lib/rack/utils.rb, line 475
def secure_compare(a, b)
  return false unless a.bytesize == b.bytesize

  OpenSSL.fixed_length_secure_compare(a, b)
end
# File lib/rack/utils.rb, line 202
def select_best_encoding(available_encodings, accept_encoding)
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

  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, p| [-q, p] }
    .map!(&:first)

  unless encoding_candidates.include?("identity")
    encoding_candidates.push("identity")
  end

  expanded_accept_encoding.each do |m, q|
    encoding_candidates.delete(m) if q == 0.0
  end

  (encoding_candidates & available_encodings)[0]
end
# File lib/rack/utils.rb, line 625
def status_code(status)
  if status.is_a?(Symbol)
    SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
  else
    status.to_i
  end
end

Unescapes a URI escaped string with encoding. encoding will be the target encoding of the string returned, and it defaults to UTF-8

# File lib/rack/utils.rb, line 55
def unescape(s, encoding = Encoding::UTF_8)
  URI.decode_www_form_component(s, encoding)
end

Unescapes the path component of a URI. See Rack::Utils.unescape for unescaping query parameters or form components.

# File lib/rack/utils.rb, line 49
def unescape_path(s)
  ::URI::DEFAULT_PARSER.unescape s
end
# File lib/rack/utils.rb, line 652
def valid_path?(path)
  path.valid_encoding? && !path.include?(NULL_BYTE)
end