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
DEFAULT_SEP =
QueryParser::DEFAULT_SEP
COMMON_SEP =
QueryParser::COMMON_SEP
KeySpaceConstrainedParams =
QueryParser::Params
RFC2822_DAY_NAME =
[ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
RFC2822_MONTH_NAME =
[ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
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.



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

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



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/rack/utils.rb', line 237

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

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



320
321
322
323
324
325
326
327
328
# File 'lib/rack/utils.rb', line 320

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



159
160
161
162
163
164
165
166
167
168
169
170
# 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) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# 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) ⇒ Object



116
117
118
119
120
121
122
123
124
# 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) ⇒ 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.



352
353
354
355
# File 'lib/rack/utils.rb', line 352

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



606
607
608
609
610
611
612
613
614
615
616
617
618
619
# File 'lib/rack/utils.rb', line 606

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


313
314
315
316
# File 'lib/rack/utils.rb', line 313

def delete_cookie_header!(header, key, value = {})
  header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
  nil
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.



184
185
186
# 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) ⇒ 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

.get_byte_ranges(http_range, size) ⇒ Object



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/rack/utils.rb', line 357

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
  ranges
end

.key_space_limitObject



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

def self.key_space_limit
  default_query_parser.key_space_limit
end

.key_space_limit=(v) ⇒ Object



92
93
94
# 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


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

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

.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



220
221
222
# File 'lib/rack/utils.rb', line 220

def parse_cookies(env)
  parse_cookies_header env[HTTP_COOKIE]
end

.parse_cookies_header(header) ⇒ Object



224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/rack/utils.rb', line 224

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



112
113
114
# 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) ⇒ Object



108
109
110
# 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) ⇒ Object



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

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 parameters && (md = /\Aq=([\d.]+)/.match(parameters))
      quality = md[1].to_f
    end
    [value, quality]
  end
end

.rfc2109(time) ⇒ Object

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.



343
344
345
346
347
# File 'lib/rack/utils.rb', line 343

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



330
331
332
# File 'lib/rack/utils.rb', line 330

def rfc2822(time)
  time.rfc2822
end

.secure_compare(a, b) ⇒ Object

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.



392
393
394
395
396
397
398
399
400
# File 'lib/rack/utils.rb', line 392

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



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

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


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

def set_cookie_header!(header, key, value)
  header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
  nil
end

.status_code(status) ⇒ Object



596
597
598
599
600
601
602
# File 'lib/rack/utils.rb', line 596

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)


623
624
625
# File 'lib/rack/utils.rb', line 623

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

Instance Method Details

#clock_timeObject

:nocov:



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

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