Module: Rack::Utils

Included in:
Chunked, 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, HeaderHash

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
ESCAPE_HTML =
{
  "&" => "&",
  "<" => "&lt;",
  ">" => "&gt;",
  "'" => "&#x27;",
  '"' => "&quot;",
  "/" => "&#x2F;"
}
ESCAPE_HTML_PATTERN =
Regexp.union(*ESCAPE_HTML.keys)
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,"'
{
  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',
  306 => '(Unused)',
  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 => 'Payload Too Large',
  414 => 'URI Too Long',
  415 => 'Unsupported Media Type',
  416 => 'Range Not Satisfiable',
  417 => 'Expectation Failed',
  421 => 'Misdirected Request',
  422 => 'Unprocessable Entity',
  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',
  509 => 'Bandwidth Limit Exceeded',
  510 => 'Not Extended',
  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.



28
29
30
# File 'lib/rack/utils.rb', line 28

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.



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

def multipart_file_limit
  @multipart_file_limit
end

.multipart_total_part_limitObject

Returns the value of attribute multipart_total_part_limit.



61
62
63
# File 'lib/rack/utils.rb', line 61

def multipart_total_part_limit
  @multipart_total_part_limit
end

Class Method Details



255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/rack/utils.rb', line 255

def add_cookie_to_header(header, key, value)
  warn("add_cookie_to_header is deprecated and will be removed in Rack 3.1", uplevel: 1)

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


390
391
392
393
394
# File 'lib/rack/utils.rb', line 390

def add_remove_cookie_to_header(header, key, value = {})
  warn("add_remove_cookie_to_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)

  delete_set_cookie_header!(header, key, value)
end

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



174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/rack/utils.rb', line 174

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



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/rack/utils.rb', line 127

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



117
118
119
120
121
122
123
124
125
# File 'lib/rack/utils.rb', line 117

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



432
433
434
# File 'lib/rack/utils.rb', line 432

def byte_ranges(env, size)
  get_byte_ranges env['HTTP_RANGE'], size
end

.clean_path_info(path_info) ⇒ Object



636
637
638
639
640
641
642
643
644
645
646
647
648
649
# File 'lib/rack/utils.rb', line 636

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


384
385
386
387
388
# File 'lib/rack/utils.rb', line 384

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"


374
375
376
# File 'lib/rack/utils.rb', line 374

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


414
415
416
417
418
419
420
421
422
423
# File 'lib/rack/utils.rb', line 414

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 +)



38
39
40
# File 'lib/rack/utils.rb', line 38

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

.escape_html(string) ⇒ Object

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



199
200
201
# File 'lib/rack/utils.rb', line 199

def escape_html(string)
  string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
end

.escape_path(s) ⇒ Object

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



44
45
46
# File 'lib/rack/utils.rb', line 44

def escape_path(s)
  ::URI::DEFAULT_PARSER.escape s
end

.forwarded_values(forwarded_header) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/rack/utils.rb', line 156

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

.get_byte_ranges(http_range, size) ⇒ Object



436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/rack/utils.rb', line 436

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

.key_space_limitObject



88
89
90
91
# File 'lib/rack/utils.rb', line 88

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

.key_space_limit=(v) ⇒ Object



93
94
95
# File 'lib/rack/utils.rb', line 93

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


378
379
380
381
382
# File 'lib/rack/utils.rb', line 378

def make_delete_cookie_header(header, key, value)
  warn("make_delete_cookie_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)

  delete_set_cookie_header!(header, key, value)
end

.param_depth_limitObject



80
81
82
# File 'lib/rack/utils.rb', line 80

def self.param_depth_limit
  default_query_parser.param_depth_limit
end

.param_depth_limit=(v) ⇒ Object



84
85
86
# 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

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


279
280
281
# File 'lib/rack/utils.rb', line 279

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


245
246
247
248
249
250
251
252
253
# File 'lib/rack/utils.rb', line 245

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



113
114
115
# File 'lib/rack/utils.rb', line 113

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



109
110
111
# File 'lib/rack/utils.rb', line 109

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

.q_values(q_value_header) ⇒ Object



145
146
147
148
149
150
151
152
153
154
# File 'lib/rack/utils.rb', line 145

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



425
426
427
# File 'lib/rack/utils.rb', line 425

def rfc2822(time)
  time.rfc2822
end

.select_best_encoding(available_encodings, accept_encoding) ⇒ Object



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/rack/utils.rb', line 203

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

: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 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).

An extra cookie attribute escape_key can be provided to control whether or not the cookie key is URL encoded. If explicitly set to false, the cookie key name will not be url encoded (escaped). The default is true.

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

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


306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/rack/utils.rb', line 306

def set_cookie_header(key, value)
  case value
  when Hash
    key = escape(key) unless value[:escape_key] == false
    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]
  else
    key = escape(key)
  end

  value = [value] unless Array === value

  return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
    "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
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.



348
349
350
351
352
353
354
355
356
357
358
# File 'lib/rack/utils.rb', line 348

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



626
627
628
629
630
631
632
# File 'lib/rack/utils.rb', line 626

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

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



56
57
58
# 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) ⇒ Object

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



50
51
52
# File 'lib/rack/utils.rb', line 50

def unescape_path(s)
  ::URI::DEFAULT_PARSER.unescape s
end

.valid_path?(path) ⇒ Boolean

Returns:

  • (Boolean)


653
654
655
# File 'lib/rack/utils.rb', line 653

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

Instance Method Details

#clock_timeObject

:nocov:



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

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

#secure_compare(a, b) ⇒ Object

:nocov:



476
477
478
479
480
# File 'lib/rack/utils.rb', line 476

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

  OpenSSL.fixed_length_secure_compare(a, b)
end