module Rack::Utils
Rack::Utils contains a grab-bag of useful methods for writing web applications adopted from all kinds of Ruby libraries.
Constants
- DEFAULT_SEP
- ESCAPE_HTML
- 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?
- 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,"'
- Multipart
#<RDoc::Comment:0x000000080755a730>
#<RDoc::Comment:0x000000080778f2d0>
#<RDoc::Comment:0x0000000806c5a1d0>
#<RDoc::Comment:0x0000000806c8bd20>
- PATH_SEPS
- STATUS_WITH_NO_ENTITY_BODY
Responses with HTTP status codes that should not have an entity body
- SYMBOL_TO_STATUS_CODE
Attributes
Public Class Methods
# File lib/rack/utils.rb, line 216 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
# File lib/rack/utils.rb, line 185 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
# File lib/rack/utils.rb, line 174 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
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.
# File lib/rack/utils.rb, line 412 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
# File lib/rack/utils.rb, line 378 def bytesize(string) string.bytesize end
# File lib/rack/utils.rb, line 677 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
URI escapes. (CGI style space to +)
# File lib/rack/utils.rb, line 34 def escape(s) URI.encode_www_form_component(s) end
Escape ampersands, brackets and quotes to their HTML/XML entities.
# File lib/rack/utils.rb, line 247 def escape_html(string) string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] } end
Like URI escaping, but with %20 instead of +. Strictly speaking this is true URI escaping.
# File lib/rack/utils.rb, line 41 def escape_path(s) escape(s).gsub('+', '%20') end
#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.
# File lib/rack/utils.rb, line 133 def normalize_params(params, name, v = nil, depth = Utils.param_depth_limit) raise RangeError if depth <= 0 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, depth - 1) else params[k] << normalize_params(params.class.new, child_key, v, depth - 1) 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, depth - 1) end return params end
# File lib/rack/utils.rb, line 169 def params_hash_type?(obj) obj.kind_of?(KeySpaceConstrainedParams) || obj.kind_of?(Hash) end
#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.
# File lib/rack/utils.rb, line 115 def parse_nested_query(qs, d = nil) params = KeySpaceConstrainedParams.new (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p| k, v = p.split('=', 2).map { |s| unescape(s) } normalize_params(params, k, v) end return params.to_params_hash rescue ArgumentError => e raise InvalidParameterError, e.message end
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 '&;').
# File lib/rack/utils.rb, line 86 def parse_query(qs, d = nil, &unescaper) unescaper ||= method(:unescape) params = KeySpaceConstrainedParams.new (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p| next if p.empty? k, v = p.split('=', 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
# File lib/rack/utils.rb, line 204 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
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.
# File lib/rack/utils.rb, line 402 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
# File lib/rack/utils.rb, line 388 def rfc2822(time) time.rfc2822 end
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.
# File lib/rack/utils.rb, line 448 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
# File lib/rack/utils.rb, line 252 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
# File lib/rack/utils.rb, line 664 def status_code(status) if status.is_a?(Symbol) SYMBOL_TO_STATUS_CODE[status] || 500 else status.to_i end end
# File lib/rack/utils.rb, line 49 def unescape(s, encoding = Encoding::UTF_8) URI.decode_www_form_component(s, encoding) end
Private Instance Methods
# File lib/rack/utils.rb, line 216 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
# File lib/rack/utils.rb, line 185 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
# File lib/rack/utils.rb, line 174 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
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.
# File lib/rack/utils.rb, line 412 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
# File lib/rack/utils.rb, line 378 def bytesize(string) string.bytesize end
# File lib/rack/utils.rb, line 677 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
URI escapes. (CGI style space to +)
# File lib/rack/utils.rb, line 34 def escape(s) URI.encode_www_form_component(s) end
Escape ampersands, brackets and quotes to their HTML/XML entities.
# File lib/rack/utils.rb, line 247 def escape_html(string) string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] } end
Like URI escaping, but with %20 instead of +. Strictly speaking this is true URI escaping.
# File lib/rack/utils.rb, line 41 def escape_path(s) escape(s).gsub('+', '%20') end
#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.
# File lib/rack/utils.rb, line 133 def normalize_params(params, name, v = nil, depth = Utils.param_depth_limit) raise RangeError if depth <= 0 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, depth - 1) else params[k] << normalize_params(params.class.new, child_key, v, depth - 1) 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, depth - 1) end return params end
# File lib/rack/utils.rb, line 169 def params_hash_type?(obj) obj.kind_of?(KeySpaceConstrainedParams) || obj.kind_of?(Hash) end
#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.
# File lib/rack/utils.rb, line 115 def parse_nested_query(qs, d = nil) params = KeySpaceConstrainedParams.new (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p| k, v = p.split('=', 2).map { |s| unescape(s) } normalize_params(params, k, v) end return params.to_params_hash rescue ArgumentError => e raise InvalidParameterError, e.message end
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 '&;').
# File lib/rack/utils.rb, line 86 def parse_query(qs, d = nil, &unescaper) unescaper ||= method(:unescape) params = KeySpaceConstrainedParams.new (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p| next if p.empty? k, v = p.split('=', 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
# File lib/rack/utils.rb, line 204 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
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.
# File lib/rack/utils.rb, line 402 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
# File lib/rack/utils.rb, line 388 def rfc2822(time) time.rfc2822 end
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.
# File lib/rack/utils.rb, line 448 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
# File lib/rack/utils.rb, line 252 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
# File lib/rack/utils.rb, line 664 def status_code(status) if status.is_a?(Symbol) SYMBOL_TO_STATUS_CODE[status] || 500 else status.to_i end end
# File lib/rack/utils.rb, line 49 def unescape(s, encoding = Encoding::UTF_8) URI.decode_www_form_component(s, encoding) end