module Rack::Utils

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

Constants

ALLOWED_FORWARED_PARAMS
COMMON_SEP
DEFAULT_SEP
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 -rcsv -e "puts CSV.parse(STDIN, headers: true) \
  .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
  .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
InvalidParameterError
KeySpaceConstrainedParams
NULL_BYTE
OBSOLETE_SYMBOLS_TO_STATUS_CODES
OBSOLETE_SYMBOL_MAPPINGS
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
URI_PARSER

A valid cookie key according to RFC2616. A <cookie-name> can be any US-ASCII characters, except control characters, spaces, or tabs. It also must not contain a separator character like the following: ( ) < > @ , ; : \ “ / [ ] ? = { }.

Attributes

default_query_parser[RW]
multipart_file_limit[RW]
multipart_part_limit[RW]
multipart_part_limit=[RW]
multipart_total_part_limit[RW]

Public Class Methods

forwarded_values(forwarded_header) click to toggle source
# File lib/rack/utils.rb, line 151
def forwarded_values(forwarded_header)
  return unless forwarded_header
  header = forwarded_header.to_s.tr("\n", ";")
  header.sub!(/\A[\s;,]+/, '')
  num_params = num_escapes = 0
  max_params = max_escapes = 1024
  params = {}

  # Parse parameter list
  while i = header.index('=')
    # Only parse up to max parameters, to avoid potential denial of service
    num_params += 1
    return if num_params > max_params

    # Found end of parameter name, ensure forward progress in loop
    param = header.slice!(0, i+1)

    # Remove ending equals and preceding whitespace from parameter name
    param.chomp!('=')
    param.strip!
    param.downcase!
    return unless param = ALLOWED_FORWARED_PARAMS[param]

    if header[0] == '"'
      # Parameter value is quoted, parse it, handling backslash escapes
      header.slice!(0, 1)
      value = String.new

      while i = header.index(/(["\\])/)
        c = $1

        # Append all content until ending quote or escape
        value << header.slice!(0, i)

        # Remove either backslash or ending quote,
        # ensures forward progress in loop
        header.slice!(0, 1)

        # stop parsing parameter value if found ending quote
        break if c == '"'

        # Only allow up to max escapes, to avoid potential denial of service
        num_escapes += 1
        return if num_escapes > max_escapes
        escaped_char = header.slice!(0, 1)
        value << escaped_char
      end
    else
      if i = header.index(/[;,]/)
        # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
        value = header.slice!(0, i)
        value.sub!(/[\s;,]+\z/, '')
      else
        # If no ending semicolon, assume remainder of line is value and stop parsing
        header.strip!
        value = header
        header = ''
      end
      value.lstrip!
    end

    (params[param] ||= []) << value

    # skip trailing semicolons/commas/whitespace, to proceed to next parameter
    header.sub!(/\A[\s;,]+/, '') unless header.empty?
  end

  params
end
param_depth_limit() click to toggle source
# File lib/rack/utils.rb, line 81
def self.param_depth_limit
  default_query_parser.param_depth_limit
end
param_depth_limit=(v) click to toggle source
# File lib/rack/utils.rb, line 85
def self.param_depth_limit=(v)
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
end

Public Instance Methods

best_q_match(q_value_header, available_mimes) click to toggle source

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 226
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
build_nested_query(value, prefix = nil) click to toggle source
# File lib/rack/utils.rb, line 119
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
build_query(params) click to toggle source
# File lib/rack/utils.rb, line 109
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
byte_ranges(env, size, max_ranges: 100) click to toggle source

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 498
def byte_ranges(env, size, max_ranges: 100)
  get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
end
clean_path_info(path_info) click to toggle source
# File lib/rack/utils.rb, line 700
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
clock_time() click to toggle source
# File lib/rack/utils.rb, line 90
def clock_time
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
escape(s) click to toggle source

URI escapes. (CGI style space to +)

# File lib/rack/utils.rb, line 39
def escape(s)
  URI.encode_www_form_component(s)
end
escape_html(string) click to toggle source

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

# File lib/rack/utils.rb, line 246
def escape_html(string)
  CGI.escapeHTML(string.to_s)
end
escape_path(s) click to toggle source

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

# File lib/rack/utils.rb, line 45
def escape_path(s)
  URI_PARSER.escape s
end
get_byte_ranges(http_range, size, max_ranges: 100) click to toggle source
# File lib/rack/utils.rb, line 502
def get_byte_ranges(http_range, size, max_ranges: 100)
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
  # Ignore Range when file size is 0 to avoid a 416 error.
  return nil if size.zero?
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
  byte_range = $1
  return nil if byte_range.count(',') >= max_ranges
  ranges = []
  byte_range.split(/,[ \t]*/).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(env) → hash click to toggle source

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 342
def parse_cookies(env)
  parse_cookies_header env[HTTP_COOKIE]
end
parse_cookies_header(value) → hash click to toggle source

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 323
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
parse_nested_query(qs, d = nil) click to toggle source
# File lib/rack/utils.rb, line 105
def parse_nested_query(qs, d = nil)
  Rack::Utils.default_query_parser.parse_nested_query(qs, d)
end
parse_query(qs, d = nil, &unescaper) click to toggle source
# File lib/rack/utils.rb, line 101
def parse_query(qs, d = nil, &unescaper)
  Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
end
q_values(q_value_header) click to toggle source
# File lib/rack/utils.rb, line 137
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
rfc2822(time) click to toggle source
# File lib/rack/utils.rb, line 491
def rfc2822(time)
  time.rfc2822
end
secure_compare(a, b) click to toggle source

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 546
def secure_compare(a, b)
  return false unless a.bytesize == b.bytesize

  OpenSSL.fixed_length_secure_compare(a, b)
end
select_best_encoding(available_encodings, accept_encoding) click to toggle source

Given an array of available encoding strings, and an array of acceptable encodings for a request, where each element of the acceptable encodings array is an array where the first element is an encoding name and the second element is the numeric priority for the encoding, return the available encoding with the highest priority.

The accept_encoding argument is typically generated by calling Request#accept_encoding.

Example:

select_best_encoding(%w(compress gzip identity),
                     [["compress", 0.5], ["gzip", 1.0]])
# => "gzip"

To reduce denial of service potential, only the first 16 acceptable encodings are considered.

# File lib/rack/utils.rb, line 269
def select_best_encoding(available_encodings, accept_encoding)
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

  # Only process the first 16 encodings
  accept_encoding = accept_encoding[0...16]
  expanded_accept_encoding = []
  wildcard_seen = false

  accept_encoding.each do |m, q|
    preference = available_encodings.index(m) || available_encodings.size

    if m == "*"
      unless wildcard_seen
        (available_encodings - accept_encoding.map(&:first)).each do |m2|
          expanded_accept_encoding << [m2, q, preference]
        end
        wildcard_seen = true
      end
    else
      expanded_accept_encoding << [m, q, preference]
    end
  end

  encoding_candidates = expanded_accept_encoding
    .sort do |(_, q1, p1), (_, q2, p2)|
      if r = (q1 <=> q2).nonzero?
        -r
      else
        (p1 <=> p2).nonzero? || 0
      end
    end
    .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
status_code(status) click to toggle source
# File lib/rack/utils.rb, line 680
def status_code(status)
  if status.is_a?(Symbol)
    SYMBOL_TO_STATUS_CODE.fetch(status) do
      fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
      message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
      if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
        # message = "#{message} Please use #{canonical_symbol.inspect} instead."
        # For now, let's not emit any warning when there is a mapping.
      else
        warn message, uplevel: 3
      end
      fallback_code
    end
  else
    status.to_i
  end
end
unescape(s, encoding = Encoding::UTF_8) click to toggle source

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 57
def unescape(s, encoding = Encoding::UTF_8)
  URI.decode_www_form_component(s, encoding)
end
unescape_path(s) click to toggle source

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

# File lib/rack/utils.rb, line 51
def unescape_path(s)
  URI_PARSER.unescape s
end
valid_path?(path) click to toggle source
# File lib/rack/utils.rb, line 717
def valid_path?(path)
  path.valid_encoding? && !path.include?(NULL_BYTE)
end

Private Instance Methods

forwarded_values(forwarded_header) click to toggle source
# File lib/rack/utils.rb, line 151
def forwarded_values(forwarded_header)
  return unless forwarded_header
  header = forwarded_header.to_s.tr("\n", ";")
  header.sub!(/\A[\s;,]+/, '')
  num_params = num_escapes = 0
  max_params = max_escapes = 1024
  params = {}

  # Parse parameter list
  while i = header.index('=')
    # Only parse up to max parameters, to avoid potential denial of service
    num_params += 1
    return if num_params > max_params

    # Found end of parameter name, ensure forward progress in loop
    param = header.slice!(0, i+1)

    # Remove ending equals and preceding whitespace from parameter name
    param.chomp!('=')
    param.strip!
    param.downcase!
    return unless param = ALLOWED_FORWARED_PARAMS[param]

    if header[0] == '"'
      # Parameter value is quoted, parse it, handling backslash escapes
      header.slice!(0, 1)
      value = String.new

      while i = header.index(/(["\\])/)
        c = $1

        # Append all content until ending quote or escape
        value << header.slice!(0, i)

        # Remove either backslash or ending quote,
        # ensures forward progress in loop
        header.slice!(0, 1)

        # stop parsing parameter value if found ending quote
        break if c == '"'

        # Only allow up to max escapes, to avoid potential denial of service
        num_escapes += 1
        return if num_escapes > max_escapes
        escaped_char = header.slice!(0, 1)
        value << escaped_char
      end
    else
      if i = header.index(/[;,]/)
        # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
        value = header.slice!(0, i)
        value.sub!(/[\s;,]+\z/, '')
      else
        # If no ending semicolon, assume remainder of line is value and stop parsing
        header.strip!
        value = header
        header = ''
      end
      value.lstrip!
    end

    (params[param] ||= []) << value

    # skip trailing semicolons/commas/whitespace, to proceed to next parameter
    header.sub!(/\A[\s;,]+/, '') unless header.empty?
  end

  params
end