Module: Rack::Utils

Included in:
ContentLength, ContentType
Defined in:
lib/rack/utils.rb

Overview

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

Defined Under Namespace

Classes: Context

Constant Summary collapse

ParameterTypeError =
QueryParser::ParameterTypeError
InvalidParameterError =
QueryParser::InvalidParameterError
ParamsTooDeepError =
QueryParser::ParamsTooDeepError
DEFAULT_SEP =
QueryParser::DEFAULT_SEP
COMMON_SEP =
QueryParser::COMMON_SEP
KeySpaceConstrainedParams =
QueryParser::Params
URI_PARSER =
defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER
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)"
{
  100 => 'Continue',
  101 => 'Switching Protocols',
  102 => 'Processing',
  103 => 'Early Hints',
  200 => 'OK',
  201 => 'Created',
  202 => 'Accepted',
  203 => 'Non-Authoritative Information',
  204 => 'No Content',
  205 => 'Reset Content',
  206 => 'Partial Content',
  207 => 'Multi-Status',
  208 => 'Already Reported',
  226 => 'IM Used',
  300 => 'Multiple Choices',
  301 => 'Moved Permanently',
  302 => 'Found',
  303 => 'See Other',
  304 => 'Not Modified',
  305 => 'Use Proxy',
  307 => 'Temporary Redirect',
  308 => 'Permanent Redirect',
  400 => 'Bad Request',
  401 => 'Unauthorized',
  402 => 'Payment Required',
  403 => 'Forbidden',
  404 => 'Not Found',
  405 => 'Method Not Allowed',
  406 => 'Not Acceptable',
  407 => 'Proxy Authentication Required',
  408 => 'Request Timeout',
  409 => 'Conflict',
  410 => 'Gone',
  411 => 'Length Required',
  412 => 'Precondition Failed',
  413 => 'Content Too Large',
  414 => 'URI Too Long',
  415 => 'Unsupported Media Type',
  416 => 'Range Not Satisfiable',
  417 => 'Expectation Failed',
  421 => 'Misdirected Request',
  422 => 'Unprocessable Content',
  423 => 'Locked',
  424 => 'Failed Dependency',
  425 => 'Too Early',
  426 => 'Upgrade Required',
  428 => 'Precondition Required',
  429 => 'Too Many Requests',
  431 => 'Request Header Fields Too Large',
  451 => 'Unavailable For Legal Reasons',
  500 => 'Internal Server Error',
  501 => 'Not Implemented',
  502 => 'Bad Gateway',
  503 => 'Service Unavailable',
  504 => 'Gateway Timeout',
  505 => 'HTTP Version Not Supported',
  506 => 'Variant Also Negotiates',
  507 => 'Insufficient Storage',
  508 => 'Loop Detected',
  511 => 'Network Authentication Required'
}
STATUS_WITH_NO_ENTITY_BODY =

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

SYMBOL_TO_STATUS_CODE =
PATH_SEPS =
Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
NULL_BYTE =
"\0"

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.default_query_parserObject

Returns the value of attribute default_query_parser.



30
31
32
# File 'lib/rack/utils.rb', line 30

def default_query_parser
  @default_query_parser
end

.multipart_file_limitObject Also known as: multipart_part_limit

Returns the value of attribute multipart_file_limit.



65
66
67
# File 'lib/rack/utils.rb', line 65

def multipart_file_limit
  @multipart_file_limit
end

.multipart_total_part_limitObject

Returns the value of attribute multipart_total_part_limit.



63
64
65
# File 'lib/rack/utils.rb', line 63

def multipart_total_part_limit
  @multipart_total_part_limit
end

Class Method Details

.best_q_match(q_value_header, available_mimes) ⇒ Object

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.



227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/rack/utils.rb', line 227

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) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/rack/utils.rb', line 120

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) ⇒ Object



110
111
112
113
114
115
116
117
118
# File 'lib/rack/utils.rb', line 110

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) ⇒ Object

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.



492
493
494
# File 'lib/rack/utils.rb', line 492

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) ⇒ Object



692
693
694
695
696
697
698
699
700
701
702
703
704
705
# File 'lib/rack/utils.rb', line 692

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


450
451
452
453
454
# File 'lib/rack/utils.rb', line 450

def delete_cookie_header!(headers, key, value = {})
  headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)

  return nil
end

:call-seq:

delete_set_cookie_header(key, value = {}) -> encoded string

Generate an encoded string based on the given key and value using set_cookie_header for the purpose of causing the specified cookie to be deleted. The value may be an instance of Hash and can include attributes as outlined by set_cookie_header. The encoded cookie will have a max_age of 0 seconds, an expires date in the past and an empty value. When used with the set-cookie header, it will cause the client to remove any matching cookie.

delete_set_cookie_header("myname")
# => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"


446
447
448
# File 'lib/rack/utils.rb', line 446

def delete_set_cookie_header(key, value = {})
  set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
end

:call-seq:

delete_set_cookie_header!(header, key, value = {}) -> header value

Set an expired cookie in the specified headers with the given cookie key and value using delete_set_cookie_header. This causes the client to immediately delete the specified cookie.

delete_set_cookie_header!(nil, "mycookie")
# => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"

If the header is non-nil, it will be modified in place.

header = []
delete_set_cookie_header!(header, "mycookie")
# => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
header
# => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]


474
475
476
477
478
479
480
481
482
483
# File 'lib/rack/utils.rb', line 474

def delete_set_cookie_header!(header, key, value = {})
  if header
    header = Array(header)
    header << delete_set_cookie_header(key, value)
  else
    header = delete_set_cookie_header(key, value)
  end

  return header
end

.escape(s) ⇒ Object

URI escapes. (CGI style space to +)



40
41
42
# File 'lib/rack/utils.rb', line 40

def escape(s)
  URI.encode_www_form_component(s)
end

.escape_path(s) ⇒ Object

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



46
47
48
# File 'lib/rack/utils.rb', line 46

def escape_path(s)
  URI_PARSER.escape s
end

.forwarded_values(forwarded_header) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/rack/utils.rb', line 152

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

.get_byte_ranges(http_range, size, max_ranges: 100) ⇒ Object



496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/rack/utils.rb', line 496

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

.param_depth_limitObject



82
83
84
# File 'lib/rack/utils.rb', line 82

def self.param_depth_limit
  default_query_parser.param_depth_limit
end

.param_depth_limit=(v) ⇒ Object



86
87
88
# File 'lib/rack/utils.rb', line 86

def self.param_depth_limit=(v)
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
end

.parse_cookies(env) ⇒ Object

:call-seq:

parse_cookies(env) -> hash

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'}


347
348
349
# File 'lib/rack/utils.rb', line 347

def parse_cookies(env)
  parse_cookies_header env[HTTP_COOKIE]
end

.parse_cookies_header(value) ⇒ Object

:call-seq:

parse_cookies_header(value) -> hash

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"}


328
329
330
331
332
333
334
335
336
# File 'lib/rack/utils.rb', line 328

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) ⇒ Object



106
107
108
# File 'lib/rack/utils.rb', line 106

def parse_nested_query(qs, d = nil)
  Rack::Utils.default_query_parser.parse_nested_query(qs, d)
end

.parse_query(qs, d = nil, &unescaper) ⇒ Object



102
103
104
# File 'lib/rack/utils.rb', line 102

def parse_query(qs, d = nil, &unescaper)
  Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
end

.q_values(q_value_header) ⇒ Object



138
139
140
141
142
143
144
145
146
147
# File 'lib/rack/utils.rb', line 138

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) ⇒ Object



485
486
487
# File 'lib/rack/utils.rb', line 485

def rfc2822(time)
  time.rfc2822
end

.select_best_encoding(available_encodings, accept_encoding) ⇒ Object

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.



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/rack/utils.rb', line 274

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

:call-seq:

set_cookie_header(key, value) -> encoded string

Generate an encoded string using the provided key and value suitable for the set-cookie header according to RFC6265. The value may be an instance of either String or Hash. If the cookie key is invalid (as defined by RFC6265), an ArgumentError will be raised.

If the cookie value is an instance of Hash, it considers the following cookie attribute keys: domain, max_age, expires (must be instance of Time), secure, http_only, same_site and value. For more details about the interpretation of these fields, consult [RFC6265 Section 5.2](datatracker.ietf.org/doc/html/rfc6265#section-5.2).

set_cookie_header("myname", "myvalue")
# => "myname=myvalue"

set_cookie_header("myname", {value: "myvalue", max_age: 10})
# => "myname=myvalue; max-age=10"


376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/rack/utils.rb', line 376

def set_cookie_header(key, value)
  unless key =~ VALID_COOKIE_KEY
    raise ArgumentError, "invalid cookie key: #{key.inspect}"
  end

  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 :same_site value: #{value[:same_site].inspect}"
      end
    partitioned = "; partitioned" if value[:partitioned]
    value = value[:value]
  end

  value = [value] unless Array === value

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

:call-seq:

set_cookie_header!(headers, key, value) -> header value

Append a cookie in the specified headers with the given cookie key and value using set_cookie_header.

If the headers already contains a set-cookie key, it will be converted to an Array if not already, and appended to.



420
421
422
423
424
425
426
427
428
429
430
# File 'lib/rack/utils.rb', line 420

def set_cookie_header!(headers, key, value)
  if header = headers[SET_COOKIE]
    if header.is_a?(Array)
      header << set_cookie_header(key, value)
    else
      headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
    end
  else
    headers[SET_COOKIE] = set_cookie_header(key, value)
  end
end

.status_code(status) ⇒ Object



674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
# File 'lib/rack/utils.rb', line 674

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."
      end
      warn message, uplevel: 3
      fallback_code
    end
  else
    status.to_i
  end
end

.unescape(s, encoding = Encoding::UTF_8) ⇒ Object

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



58
59
60
# File 'lib/rack/utils.rb', line 58

def unescape(s, encoding = Encoding::UTF_8)
  URI.decode_www_form_component(s, encoding)
end

.unescape_path(s) ⇒ Object

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



52
53
54
# File 'lib/rack/utils.rb', line 52

def unescape_path(s)
  URI_PARSER.unescape s
end

.valid_path?(path) ⇒ Boolean

Returns:

  • (Boolean)


709
710
711
# File 'lib/rack/utils.rb', line 709

def valid_path?(path)
  path.valid_encoding? && !path.include?(NULL_BYTE)
end

Instance Method Details

#clock_timeObject

:nocov:



96
97
98
# File 'lib/rack/utils.rb', line 96

def clock_time
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
end

#escape_html(string) ⇒ Object

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



250
251
252
# File 'lib/rack/utils.rb', line 250

def escape_html(string)
  CGI.escapeHTML(string.to_s)
end

#secure_compare(a, b) ⇒ Object

:nocov:



540
541
542
543
544
# File 'lib/rack/utils.rb', line 540

def secure_compare(a, b)
  return false unless a.bytesize == b.bytesize

  OpenSSL.fixed_length_secure_compare(a, b)
end