# 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
RFC2396_PARSER
RFC2822_DAY_NAME
RFC2822_MONTH_NAME
STATUS_WITH_NO_ENTITY_BODY

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

SYMBOL_TO_STATUS_CODE

### 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

key_space_limit() click to toggle source

    # File lib/rack/utils.rb, line 88
    def self.key_space_limit
      default_query_parser.key_space_limit
    end

key_space_limit=(v) click to toggle source

    # File lib/rack/utils.rb, line 92
    def self.key_space_limit=(v)
      self.default_query_parser = self.default_query_parser.new_space_limit(v)
    end

param_depth_limit() click to toggle source

    # File lib/rack/utils.rb, line 80
    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 84
    def self.param_depth_limit=(v)
      self.default_query_parser = self.default_query_parser.new_depth_limit(v)
    end

### Public Instance Methods

add_cookie_to_header(header, key, value) click to toggle source

    # File lib/rack/utils.rb, line 267
    def add_cookie_to_header(header, key, value)
      case value
      when Hash
        domain  = "; domain=#{value[:domain]}"   if value[:domain]
        path    = "; path=#{value[:path]}"       if value[:path]
        max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
        expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
        secure = "; secure"  if value[:secure]
        httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
        same_site =
          case value[:same_site]
          when false, nil
            nil
          when :none, 'None', :None
            '; SameSite=None'
          when :lax, 'Lax', :Lax
            '; SameSite=Lax'
          when true, :strict, 'Strict', :Strict
            '; SameSite=Strict'
          else
            raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
          end
        value = value[:value]
      end
      value = [value] unless Array === value

      cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
        "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"

      case header
      when nil, ''
        cookie
      when String
        [header, cookie].join("\n")
      when Array
        (header + [cookie]).join("\n")
      else
        raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
      end
    end

add_remove_cookie_to_header(header, key, value = {}) click to toggle source

Adds a cookie that will remove a cookie from the client. Hence the strange method name.

    # File lib/rack/utils.rb, line 350
    def add_remove_cookie_to_header(header, key, value = {})
      new_header = make_delete_cookie_header(header, key, value)

      add_cookie_to_header(new_header, key,
                 { value: '', path: nil, domain: nil,
                   max_age: '0',
                   expires: Time.at(0) }.merge(value))

    end

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 159
    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 && matches.first
    end

build_nested_query(value, prefix = nil) click to toggle source

    # 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}[#{escape(k)}]" : escape(k))
        }.delete_if(&:empty?).join('&')
      when nil
        prefix
      else
        raise ArgumentError, "value must be a Hash" if prefix.nil?
        "#{prefix}=#{escape(value)}"
      end
    end

build_query(params) click to toggle source

    # 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

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 382
    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 640
    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 97
    def clock_time
      Process.clock_gettime(Process::CLOCK_MONOTONIC)
    end

delete_cookie_header!(header, key, value = {}) click to toggle source

    # File lib/rack/utils.rb, line 343
    def delete_cookie_header!(header, key, value = {})
      header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
      nil
    end

escape(s) click to toggle source

URI escapes. (CGI style space to +)

    # File lib/rack/utils.rb, line 38
    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 184
    def escape_html(string)
      string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
    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 44
    def escape_path(s)
      RFC2396_PARSER.escape s
    end

get_byte_ranges(http_range, size, max_ranges: 100) click to toggle source

    # File lib/rack/utils.rb, line 386
    def get_byte_ranges(http_range, size, max_ranges: 100)
      # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
      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).inject(0, :+) > size

      ranges
    end

make_delete_cookie_header(header, key, value) click to toggle source

    # File lib/rack/utils.rb, line 313
    def make_delete_cookie_header(header, key, value)
      case header
      when nil, ''
        cookies = []
      when String
        cookies = header.split("\n")
      when Array
        cookies = header
      end

      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#{key}=/
               end

      cookies.reject! { |cookie| regexp.match? cookie }

      cookies.join("\n")
    end

parse_cookies(env) click to toggle source

    # File lib/rack/utils.rb, line 250
    def parse_cookies(env)
      parse_cookies_header env[HTTP_COOKIE]
    end

parse_cookies_header(header) click to toggle source

    # File lib/rack/utils.rb, line 254
    def parse_cookies_header(header)
      # According to RFC 6265:
      # The syntax for cookie headers only supports semicolons
      # User Agent -> Server ==
      # Cookie: SID=31d4d96e407aad42; lang=en-US
      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
    end

parse_nested_query(qs, d = nil) click to toggle source

    # 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

parse_query(qs, d = nil, &unescaper) click to toggle source

    # 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

q_values(q_value_header) click to toggle source

    # 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

rfc2109(time) click to toggle source

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.

NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough that I'm certain someone implemented only that option. Do not use %a and %b from Time.strptime, it would use localized names for weekday and month.

    # File lib/rack/utils.rb, line 373
    def rfc2109(time)
      wday = RFC2822_DAY_NAME[time.wday]
      mon = RFC2822_MONTH_NAME[time.mon - 1]
      time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
    end

rfc2822(time) click to toggle source

    # File lib/rack/utils.rb, line 360
    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 426
    def secure_compare(a, b)
      return false unless a.bytesize == b.bytesize

      l = a.unpack("C*")

      r, i = 0, -1
      b.each_byte { |v| r |= v ^ l[i += 1] }
      r == 0
    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 206
    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

set_cookie_header!(header, key, value) click to toggle source

    # File lib/rack/utils.rb, line 308
    def set_cookie_header!(header, key, value)
      header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
      nil
    end

status_code(status) click to toggle source

    # File lib/rack/utils.rb, line 630
    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

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 56
    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 50
    def unescape_path(s)
      RFC2396_PARSER.unescape s
    end

valid_path?(path) click to toggle source

    # File lib/rack/utils.rb, line 657
    def valid_path?(path)
      path.valid_encoding? && !path.include?(NULL_BYTE)
    end