module Rack::Utils
Rack::Utils contains a grab-bag of useful methods for writing web applications adopted from all kinds of Ruby libraries.
Constants
- COMMON_SEP
- DEFAULT_SEP
- 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 -rcsv -e "puts CSV.parse(STDIN, headers: true) \ .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \ .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)" - InvalidParameterError
- KeySpaceConstrainedParams
- NULL_BYTE
- PATH_SEPS
- ParameterTypeError
- ParamsTooDeepError
- STATUS_WITH_NO_ENTITY_BODY
-
Responses with HTTP status codes that should not have an entity body
- SYMBOL_TO_STATUS_CODE
- URI_PARSER
Attributes
Public Class Methods
Source
# File lib/rack/utils.rb, line 151 def forwarded_values(forwarded_header) return unless forwarded_header header = forwarded_header.to_s.tr("\n", ";") header.sub!(/\A[\s;,]+/, '') num_params = num_escapes = 0 max_params = max_escapes = 1024 params = {} # Parse parameter list while i = header.index('=') # Only parse up to max parameters, to avoid potential denial of service num_params += 1 return if num_params > max_params # Found end of parameter name, ensure forward progress in loop param = header.slice!(0, i+1) # Remove ending equals and preceding whitespace from parameter name param.chomp!('=') param.strip! param.downcase! return unless param = ALLOWED_FORWARED_PARAMS[param] if header[0] == '"' # Parameter value is quoted, parse it, handling backslash escapes header.slice!(0, 1) value = String.new while i = header.index(/(["\\])/) c = $1 # Append all content until ending quote or escape value << header.slice!(0, i) # Remove either backslash or ending quote, # ensures forward progress in loop header.slice!(0, 1) # stop parsing parameter value if found ending quote break if c == '"' # Only allow up to max escapes, to avoid potential denial of service num_escapes += 1 return if num_escapes > max_escapes escaped_char = header.slice!(0, 1) value << escaped_char end else if i = header.index(/[;,]/) # Parameter value unquoted (which may be invalid), value ends at comma or semicolon value = header.slice!(0, i) value.sub!(/[\s;,]+\z/, '') else # If no ending semicolon, assume remainder of line is value and stop parsing header.strip! value = header header = '' end value.lstrip! end (params[param] ||= []) << value # skip trailing semicolons/commas/whitespace, to proceed to next parameter header.sub!(/\A[\s;,]+/, '') unless header.empty? end params end
Source
# File lib/rack/utils.rb, line 81 def self.param_depth_limit default_query_parser.param_depth_limit end
Source
# File lib/rack/utils.rb, line 85 def self.param_depth_limit=(v) self.default_query_parser = self.default_query_parser.new_depth_limit(v) end
Public Instance Methods
Source
# File lib/rack/utils.rb, line 226 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
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.
Source
# File lib/rack/utils.rb, line 119 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
Source
# File lib/rack/utils.rb, line 109 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
Source
# File lib/rack/utils.rb, line 498 def byte_ranges(env, size, max_ranges: 100) get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges 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.
Source
# File lib/rack/utils.rb, line 700 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
Source
# File lib/rack/utils.rb, line 90 def clock_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end
Source
# File lib/rack/utils.rb, line 39 def escape(s) URI.encode_www_form_component(s) end
URI escapes. (CGI style space to +)
Source
# File lib/rack/utils.rb, line 246 def escape_html(string) CGI.escapeHTML(string.to_s) end
Escape ampersands, brackets and quotes to their HTML/XML entities.
Source
# File lib/rack/utils.rb, line 45 def escape_path(s) URI_PARSER.escape s end
Like URI escaping, but with %20 instead of +. Strictly speaking this is true URI escaping.
Source
# File lib/rack/utils.rb, line 502 def get_byte_ranges(http_range, size, max_ranges: 100) # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35> # Ignore Range when file size is 0 to avoid a 416 error. return nil if size.zero? return nil unless http_range && http_range =~ /bytes=([^;]+)/ byte_range = $1 return nil if byte_range.count(',') >= max_ranges ranges = [] byte_range.split(/,[ \t]*/).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
Source
# File lib/rack/utils.rb, line 105 def parse_nested_query(qs, d = nil) Rack::Utils.default_query_parser.parse_nested_query(qs, d) end
Source
# File lib/rack/utils.rb, line 101 def parse_query(qs, d = nil, &unescaper) Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper) end
Source
# File lib/rack/utils.rb, line 137 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
Source
# File lib/rack/utils.rb, line 546 def secure_compare(a, b) return false unless a.bytesize == b.bytesize OpenSSL.fixed_length_secure_compare(a, b) 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.
Source
# File lib/rack/utils.rb, line 269 def select_best_encoding(available_encodings, accept_encoding) # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html # Only process the first 16 encodings accept_encoding = accept_encoding[0...16] expanded_accept_encoding = [] wildcard_seen = false accept_encoding.each do |m, q| preference = available_encodings.index(m) || available_encodings.size if m == "*" unless wildcard_seen (available_encodings - accept_encoding.map(&:first)).each do |m2| expanded_accept_encoding << [m2, q, preference] end wildcard_seen = true end else expanded_accept_encoding << [m, q, preference] end end encoding_candidates = expanded_accept_encoding .sort do |(_, q1, p1), (_, q2, p2)| if r = (q1 <=> q2).nonzero? -r else (p1 <=> p2).nonzero? || 0 end end .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
Given an array of available encoding strings, and an array of acceptable encodings for a request, where each element of the acceptable encodings array is an array where the first element is an encoding name and the second element is the numeric priority for the encoding, return the available encoding with the highest priority.
The accept_encoding argument is typically generated by calling Request#accept_encoding.
Example:
select_best_encoding(%w(compress gzip identity), [["compress", 0.5], ["gzip", 1.0]]) # => "gzip"
To reduce denial of service potential, only the first 16 acceptable encodings are considered.
Source
# File lib/rack/utils.rb, line 680 def status_code(status) if status.is_a?(Symbol) SYMBOL_TO_STATUS_CODE.fetch(status) do fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" } message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack." if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status] # message = "#{message} Please use #{canonical_symbol.inspect} instead." # For now, let's not emit any warning when there is a mapping. else warn message, uplevel: 3 end fallback_code end else status.to_i end end
Source
# File lib/rack/utils.rb, line 57 def unescape(s, encoding = Encoding::UTF_8) URI.decode_www_form_component(s, encoding) end
Unescapes a URI escaped string with encoding. encoding will be the target encoding of the string returned, and it defaults to UTF-8
Source
# File lib/rack/utils.rb, line 51 def unescape_path(s) URI_PARSER.unescape s end
Unescapes the path component of a URI. See Rack::Utils.unescape for unescaping query parameters or form components.
Source
# File lib/rack/utils.rb, line 717 def valid_path?(path) path.valid_encoding? && !path.include?(NULL_BYTE) end