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 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
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_part_limit[RW]

Public Class Methods

best_q_match(q_value_header, available_mimes) click to toggle source
# File lib/rack/utils.rb, line 142
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 111
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))
    }.reject(&: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 100
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) 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 354
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
clean_path_info(path_info) click to toggle source
# File lib/rack/utils.rb, line 595
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.unshift '/' if parts.empty? || parts.first.empty?

  ::File.join(*clean)
end
clock_time() click to toggle source
# File lib/rack/utils.rb, line 80
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 27
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 168
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 34
def escape_path(s)
  ::URI::DEFAULT_PARSER.escape s
end
get_byte_ranges(http_range, size) click to toggle source
# File lib/rack/utils.rb, line 360
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 =~ /(\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.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
      end
    end
    ranges << (r0..r1)  if r0 <= r1
  end
  ranges
end
key_space_limit() click to toggle source
# File lib/rack/utils.rb, line 71
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 75
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 63
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 67
def self.param_depth_limit=(v)
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
end
parse_cookies(env) click to toggle source
# File lib/rack/utils.rb, line 201
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 206
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.
  cookies = parse_query(header, ';,') { |s| unescape(s) rescue s }
  cookies.each_with_object({}) { |(k,v), hash| hash[k] = Array === v ? v.first : v }
end
parse_nested_query(qs, d = nil) click to toggle source
# File lib/rack/utils.rb, line 95
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 90
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 130
def q_values(q_value_header)
  q_value_header.to_s.split(/\s*,\s*/).map do |part|
    value, parameters = part.split(/\s*;\s*/, 2)
    quality = 1.0
    if 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 344
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
rfc2822(time) click to toggle source
# File lib/rack/utils.rb, line 330
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 395
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
# File lib/rack/utils.rb, line 173
def select_best_encoding(available_encodings, accept_encoding)
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

  expanded_accept_encoding =
    accept_encoding.map { |m, q|
      if m == "*"
        (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
      else
        [[m, q]]
      end
    }.inject([]) { |mem, list|
      mem + list
    }

  encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }

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

  expanded_accept_encoding.each { |m, q|
    encoding_candidates.delete(m) if q == 0.0
  }

  return (encoding_candidates & available_encodings)[0]
end
status_code(status) click to toggle source
# File lib/rack/utils.rb, line 584
def status_code(status)
  if status.is_a?(Symbol)
    SYMBOL_TO_STATUS_CODE[status] || 500
  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 49
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 #unescape for unescaping query parameters or form components.

# File lib/rack/utils.rb, line 41
def unescape_path(s)
  ::URI::DEFAULT_PARSER.unescape s
end
valid_path?(path) click to toggle source
# File lib/rack/utils.rb, line 613
def valid_path?(path)
  path.valid_encoding? && !path.include?(NULL_BYTE)
end

Private Instance Methods

best_q_match(q_value_header, available_mimes) click to toggle source
# File lib/rack/utils.rb, line 142
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 111
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))
    }.reject(&: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 100
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) 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 354
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
clean_path_info(path_info) click to toggle source
# File lib/rack/utils.rb, line 595
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.unshift '/' if parts.empty? || parts.first.empty?

  ::File.join(*clean)
end
clock_time() click to toggle source
# File lib/rack/utils.rb, line 80
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 27
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 168
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 34
def escape_path(s)
  ::URI::DEFAULT_PARSER.escape s
end
get_byte_ranges(http_range, size) click to toggle source
# File lib/rack/utils.rb, line 360
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 =~ /(\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.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
      end
    end
    ranges << (r0..r1)  if r0 <= r1
  end
  ranges
end
parse_cookies(env) click to toggle source
# File lib/rack/utils.rb, line 201
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 206
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.
  cookies = parse_query(header, ';,') { |s| unescape(s) rescue s }
  cookies.each_with_object({}) { |(k,v), hash| hash[k] = Array === v ? v.first : v }
end
parse_nested_query(qs, d = nil) click to toggle source
# File lib/rack/utils.rb, line 95
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 90
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 130
def q_values(q_value_header)
  q_value_header.to_s.split(/\s*,\s*/).map do |part|
    value, parameters = part.split(/\s*;\s*/, 2)
    quality = 1.0
    if 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 344
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
rfc2822(time) click to toggle source
# File lib/rack/utils.rb, line 330
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 395
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
# File lib/rack/utils.rb, line 173
def select_best_encoding(available_encodings, accept_encoding)
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

  expanded_accept_encoding =
    accept_encoding.map { |m, q|
      if m == "*"
        (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
      else
        [[m, q]]
      end
    }.inject([]) { |mem, list|
      mem + list
    }

  encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }

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

  expanded_accept_encoding.each { |m, q|
    encoding_candidates.delete(m) if q == 0.0
  }

  return (encoding_candidates & available_encodings)[0]
end
status_code(status) click to toggle source
# File lib/rack/utils.rb, line 584
def status_code(status)
  if status.is_a?(Symbol)
    SYMBOL_TO_STATUS_CODE[status] || 500
  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 49
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 #unescape for unescaping query parameters or form components.

# File lib/rack/utils.rb, line 41
def unescape_path(s)
  ::URI::DEFAULT_PARSER.unescape s
end
valid_path?(path) click to toggle source
# File lib/rack/utils.rb, line 613
def valid_path?(path)
  path.valid_encoding? && !path.include?(NULL_BYTE)
end