Module: Rack::Utils

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

ParameterTypeError =
QueryParser::ParameterTypeError
InvalidParameterError =
QueryParser::InvalidParameterError
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 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',
  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 => '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',
  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',
  510 => 'Not Extended',
  511 => 'Network Authentication Required'
}
STATUS_WITH_NO_ENTITY_BODY =

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

Set.new((100..199).to_a << 204 << 205 << 304)
SYMBOL_TO_STATUS_CODE =
Hash[*HTTP_STATUS_CODES.map { |code, message|
  [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
}.flatten]
PATH_SEPS =
Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
NULL_BYTE =
"\0".freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.default_query_parserObject

Returns the value of attribute default_query_parser



21
22
23
# File 'lib/rack/utils.rb', line 21

def default_query_parser
  @default_query_parser
end

.multipart_part_limitObject

Returns the value of attribute multipart_part_limit



56
57
58
# File 'lib/rack/utils.rb', line 56

def multipart_part_limit
  @multipart_part_limit
end

Class Method Details



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
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
277
278
279
# File 'lib/rack/utils.rb', line 218

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]
    # There is an RFC mess in the area of date formatting for Cookies. Not
    # only are there contradicting RFCs and examples within RFC text, but
    # there are also numerous conflicting names of fields and partially
    # cross-applicable specifications.
    #
    # These are best described in RFC 2616 3.3.1. This RFC text also
    # specifies that RFC 822 as updated by RFC 1123 is preferred. That is a
    # fixed length format with space-date delimited fields.
    #
    # See also RFC 1123 section 5.2.14.
    #
    # RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined
    # in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote
    # the space delimited format. These formats are compliant with RFC 2822.
    #
    # For reference, all involved RFCs are:
    # RFC 822
    # RFC 1123
    # RFC 2109
    # RFC 2616
    # RFC 2822
    # RFC 2965
    # RFC 6265
    expires = "; expires=" +
      rfc2822(value[:expires].clone.gmtime) 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 :lax, 'Lax', :Lax
        '; SameSite=Lax'.freeze
      when true, :strict, 'Strict', :Strict
        '; SameSite=Strict'.freeze
      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



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

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



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/rack/utils.rb', line 112

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



101
102
103
104
105
106
107
108
109
# File 'lib/rack/utils.rb', line 101

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.



355
356
357
358
# File 'lib/rack/utils.rb', line 355

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



596
597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'lib/rack/utils.rb', line 596

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_timeObject



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

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


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

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



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

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

.escape_html(string) ⇒ Object

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



169
170
171
# File 'lib/rack/utils.rb', line 169

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.



35
36
37
# File 'lib/rack/utils.rb', line 35

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

.get_byte_ranges(http_range, size) ⇒ Object



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

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_limitObject



72
73
74
# File 'lib/rack/utils.rb', line 72

def self.key_space_limit
  default_query_parser.key_space_limit
end

.key_space_limit=(v) ⇒ Object



76
77
78
# File 'lib/rack/utils.rb', line 76

def self.key_space_limit=(v)
  self.default_query_parser = self.default_query_parser.new_space_limit(v)
end


288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/rack/utils.rb', line 288

def make_delete_cookie_header(header, key, value)
  case header
  when nil, ''
    cookies = []
  when String
    cookies = header.split("\n")
  when Array
    cookies = header
  end

  cookies.reject! { |cookie|
    if value[:domain]
      cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
    elsif value[:path]
      cookie =~ /\A#{escape(key)}=.*path=#{value[:path]}/
    else
      cookie =~ /\A#{escape(key)}=/
    end
  }

  cookies.join("\n")
end

.param_depth_limitObject



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

def self.param_depth_limit
  default_query_parser.param_depth_limit
end

.param_depth_limit=(v) ⇒ Object



68
69
70
# File 'lib/rack/utils.rb', line 68

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

.parse_cookies(env) ⇒ Object



202
203
204
# File 'lib/rack/utils.rb', line 202

def parse_cookies(env)
  parse_cookies_header env[HTTP_COOKIE]
end

.parse_cookies_header(header) ⇒ Object



207
208
209
210
211
212
213
214
215
# File 'lib/rack/utils.rb', line 207

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



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

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



91
92
93
# File 'lib/rack/utils.rb', line 91

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

.q_values(q_value_header) ⇒ Object



131
132
133
134
135
136
137
138
139
140
# File 'lib/rack/utils.rb', line 131

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



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

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



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

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.



396
397
398
399
400
401
402
403
404
# File 'lib/rack/utils.rb', line 396

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



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

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


282
283
284
285
# File 'lib/rack/utils.rb', line 282

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



585
586
587
588
589
590
591
# File 'lib/rack/utils.rb', line 585

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

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



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

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.



42
43
44
# File 'lib/rack/utils.rb', line 42

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

.valid_path?(path) ⇒ Boolean

Returns:

  • (Boolean)


614
615
616
# File 'lib/rack/utils.rb', line 614

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