Module: Rack::Utils

Included in:
Chunked, Chunked::Body, ContentLength, ContentType
Defined in:
lib/rack/utils.rb,
lib/rack.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

Modules: OkJson Classes: Context, HeaderHash, InvalidParameterError, KeySpaceConstrainedParams, ParameterTypeError

Constant Summary

DEFAULT_SEP =
/[&;] */n
COMMON_SEP =
{ ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
ESCAPE_HTML =
{
  "&" => "&",
  "<" => "&lt;",
  ">" => "&gt;",
  "'" => "&#x27;",
  '"' => "&quot;",
  "/" => "&#x2F;"
}
ESCAPE_HTML_PATTERN =

On 1.8, there is a kcode = 'u' bug that allows for XSS otherwise TODO doesn't apply to jruby, so a better condition above might be preferable?

/#{Regexp.union(*ESCAPE_HTML.keys)}/n
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',
  422 => 'Unprocessable Entity',
  423 => 'Locked',
  424 => 'Failed Dependency',
  426 => 'Upgrade Required',
  428 => 'Precondition Required',
  429 => 'Too Many Requests',
  431 => 'Request Header Fields Too Large',
  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]
Multipart =
Rack::Multipart
PATH_SEPS =
Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.key_space_limitObject

Returns the value of attribute key_space_limit



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

def key_space_limit
  @key_space_limit
end

.multipart_part_limitObject

Returns the value of attribute multipart_part_limit



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

def multipart_part_limit
  @multipart_part_limit
end

Class Method Details

.best_q_match(q_value_header, available_mimes) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/rack/utils.rb', line 212

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



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/rack/utils.rb', line 181

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



170
171
172
173
174
175
176
177
178
# File 'lib/rack/utils.rb', line 170

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.



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/rack/utils.rb', line 410

def byte_ranges(env, size)
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
  http_range = env['HTTP_RANGE']
  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

.bytesize(string) ⇒ Object



376
377
378
# File 'lib/rack/utils.rb', line 376

def bytesize(string)
  string.bytesize
end

.clean_path_info(path_info) ⇒ Object



685
686
687
688
689
690
691
692
693
694
695
696
697
698
# File 'lib/rack/utils.rb', line 685

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


342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/rack/utils.rb', line 342

def delete_cookie_header!(header, key, value = {})
  case header[SET_COOKIE]
  when nil, ''
    cookies = []
  when String
    cookies = header[SET_COOKIE].split("\n")
  when Array
    cookies = header[SET_COOKIE]
  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
  }

  header[SET_COOKIE] = cookies.join("\n")

  set_cookie_header!(header, key,
             {:value => '', :path => nil, :domain => nil,
               :max_age => '0',
               :expires => Time.at(0) }.merge(value))

  nil
end

.escape(s) ⇒ Object

URI escapes. (CGI style space to +)



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

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

.escape_html(string) ⇒ Object

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



243
244
245
# File 'lib/rack/utils.rb', line 243

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.



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

def escape_path(s)
  escape(s).gsub('+', '%20')
end

.normalize_params(params, name, v = nil) ⇒ Object

normalize_params recursively expands parameters into structural types. If the structural types represented by two different parameter names are in conflict, a ParameterTypeError is raised.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/rack/utils.rb', line 131

def normalize_params(params, name, v = nil)
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
  k = $1 || ''
  after = $' || ''

  return if k.empty?

  if after == ""
    params[k] = v
  elsif after == "["
    params[name] = v
  elsif after == "[]"
    params[k] ||= []
    raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
    params[k] << v
  elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
    child_key = $1
    params[k] ||= []
    raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
    if params_hash_type?(params[k].last) && !params[k].last.key?(child_key)
      normalize_params(params[k].last, child_key, v)
    else
      params[k] << normalize_params(params.class.new, child_key, v)
    end
  else
    params[k] ||= params.class.new
    raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
    params[k] = normalize_params(params[k], after, v)
  end

  return params
end

.params_hash_type?(obj) ⇒ Boolean

Returns:

  • (Boolean)


165
166
167
# File 'lib/rack/utils.rb', line 165

def params_hash_type?(obj)
  obj.kind_of?(KeySpaceConstrainedParams) || obj.kind_of?(Hash)
end

.parse_cookies(env) ⇒ Object



276
277
278
279
280
281
282
283
284
285
286
# File 'lib/rack/utils.rb', line 276

def parse_cookies(env)
  # 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.
  Hash[].tap do |hash|
    cookies = parse_query(env[HTTP_COOKIE], ';,') { |s| unescape(s) rescue s }
    cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
  end
end

.parse_nested_query(qs, d = nil) ⇒ Object

parse_nested_query expands a query string into structural types. Supported types are Arrays, Hashes and basic value types. It is possible to supply query strings with parameters of conflicting types, in this case a ParameterTypeError is raised. Users are encouraged to return a 400 in this case.



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

def parse_nested_query(qs, d = nil)
  return {} if qs.nil? || qs.empty?
  params = KeySpaceConstrainedParams.new

  (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
    k, v = p.split('='.freeze, 2).map! { |s| unescape(s) }

    normalize_params(params, k, v)
  end

  return params.to_params_hash
rescue ArgumentError => e
  raise InvalidParameterError, e.message
end

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

Stolen from Mongrel, with some small modifications: Parses a query string by breaking it up at the '&' and ';' characters. You can also use this to parse cookies by changing the characters used in the second parameter (which defaults to '&;').



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/rack/utils.rb', line 83

def parse_query(qs, d = nil, &unescaper)
  unescaper ||= method(:unescape)

  params = KeySpaceConstrainedParams.new

  (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
    next if p.empty?
    k, v = p.split('='.freeze, 2).map!(&unescaper)

    if cur = params[k]
      if cur.class == Array
        params[k] << v
      else
        params[k] = [cur, v]
      end
    else
      params[k] = v
    end
  end

  return params.to_params_hash
end

.q_values(q_value_header) ⇒ Object



200
201
202
203
204
205
206
207
208
209
# File 'lib/rack/utils.rb', line 200

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.



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

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



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

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.



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

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

  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



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

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


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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/rack/utils.rb', line 289

def set_cookie_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 delimeted 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])
    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}"

  case header[SET_COOKIE]
  when nil, ''
    header[SET_COOKIE] = cookie
  when String
    header[SET_COOKIE] = [header[SET_COOKIE], cookie].join("\n")
  when Array
    header[SET_COOKIE] = (header[SET_COOKIE] + [cookie]).join("\n")
  end

  nil
end

.status_code(status) ⇒ Object



672
673
674
675
676
677
678
# File 'lib/rack/utils.rb', line 672

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



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