module Rack::Utils
def self.param_depth_limit
def self.param_depth_limit default_query_parser.param_depth_limit end
def self.param_depth_limit=(v)
def self.param_depth_limit=(v) self.default_query_parser = self.default_query_parser.new_depth_limit(v) end
def best_q_match(q_value_header, available_mimes)
matches (same specificity and quality), the value returned
in RFC 2616 Section 14. If there are multiple best
Return best accept value to use, based on the algorithm
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
def build_nested_query(value, prefix = nil)
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
def build_query(params)
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
def byte_ranges(env, size)
Returns nil if the header is missing or syntactically invalid.
Parses the "Range:" header, if present, into an array of Range objects.
def byte_ranges(env, size) get_byte_ranges env['HTTP_RANGE'], size end
def clean_path_info(path_info)
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
def clock_time
def clock_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end
def clock_time
def clock_time Time.now.to_f end
def delete_cookie_header!(headers, key, value = {})
def delete_cookie_header!(headers, key, value = {}) headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value) return nil end
def delete_set_cookie_header(key, value = {})
# => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
delete_set_cookie_header("myname")
to *remove* any matching cookie.
+value+. When used with the +set-cookie+ header, it will cause the client
a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
attributes as outlined by set_cookie_header. The encoded cookie will have
deleted. The +value+ may be an instance of +Hash+ and can include
set_cookie_header for the purpose of causing the specified cookie to be
Generate an encoded string based on the given +key+ and +value+ using
delete_set_cookie_header(key, value = {}) -> encoded string
:call-seq:
def delete_set_cookie_header(key, value = {}) set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: '')) end
def delete_set_cookie_header!(header, key, value = {})
# => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
header
# => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
delete_set_cookie_header!(header, "mycookie")
header = []
If the header is non-nil, it will be modified in place.
# => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
delete_set_cookie_header!(nil, "mycookie")
the client to immediately delete the specified cookie.
+key+ and +value+ using delete_set_cookie_header. This causes
Set an expired cookie in the specified headers with the given cookie
delete_set_cookie_header!(header, key, value = {}) -> header value
:call-seq:
def delete_set_cookie_header!(header, key, value = {}) if header header = Array(header) header << delete_set_cookie_header(key, value) else header = delete_set_cookie_header(key, value) end return header end
def escape(s)
def escape(s) URI.encode_www_form_component(s) end
def escape_cookie_key(key)
def escape_cookie_key(key) =~ VALID_COOKIE_KEY "Cookie key #{key.inspect} is not valid according to RFC2616; it will be escaped. This behaviour is deprecated and will be removed in a future version of Rack.", uplevel: 2 pe(key)
def escape_html(string)
def escape_html(string) CGI.escapeHTML(string.to_s) end
def escape_path(s)
Like URI escaping, but with %20 instead of +. Strictly speaking this is
def escape_path(s) ::URI::DEFAULT_PARSER.escape s end
def forwarded_values(forwarded_header)
def forwarded_values(forwarded_header) return nil unless forwarded_header forwarded_header = forwarded_header.to_s.gsub("\n", ";") forwarded_header.split(';').each_with_object({}) do |field, values| field.split(',').each do |pair| pair = pair.split('=').map(&:strip).join('=') return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i (values[$1.downcase.to_sym] ||= []) << $2 end end end
def get_byte_ranges(http_range, size)
def get_byte_ranges(http_range, size) # 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=([^;]+)/ 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 return [] if ranges.map(&:size).sum > size ranges end
def parse_cookies(env)
# => {'myname' => 'myvalue'}
parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
Parse cookies from the provided request environment using
parse_cookies(env) -> hash
:call-seq:
def parse_cookies(env) parse_cookies_header env[HTTP_COOKIE] end
def parse_cookies_header(value)
# => {"myname"=>"myvalue", "max-age"=>"0"}
parse_cookies_header('myname=myvalue; max-age=0')
cookie +key+ to cookie +value+.
syntax for cookie headers only supports semicolons. Returns a map of
Parse cookies from the provided header +value+ according to RFC6265. The
parse_cookies_header(value) -> hash
:call-seq:
def parse_cookies_header(value) return {} unless value value.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
def parse_nested_query(qs, d = nil)
def parse_nested_query(qs, d = nil) Rack::Utils.default_query_parser.parse_nested_query(qs, d) end
def parse_query(qs, d = nil, &unescaper)
def parse_query(qs, d = nil, &unescaper) Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper) end
def q_values(q_value_header)
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
def rfc2822(time)
def rfc2822(time) time.rfc2822 end
def secure_compare(a, b)
on variable length plaintext strings because it could leak length info
that have already been processed by HMAC. This should not be used
NOTE: the values compared should be of fixed length, such as strings
Constant time string comparison.
def secure_compare(a, b) return false unless a.bytesize == b.bytesize OpenSSL.fixed_length_secure_compare(a, b) end
def secure_compare(a, b)
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
def select_best_encoding(available_encodings, accept_encoding)
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
def set_cookie_header(key, value)
# => "myname=myvalue; max-age=10"
set_cookie_header("myname", {value: "myvalue", max_age: 10})
# => "myname=myvalue"
set_cookie_header("myname", "myvalue")
cookie key name will not be url encoded (escaped). The default is +true+.
or not the cookie key is URL encoded. If explicitly set to +false+, the
An extra cookie attribute +escape_key+ can be provided to control whether
[RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
details about the interpretation of these fields, consult
of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
If the cookie +value+ is an instance of +Hash+, it considers the following
instance of either +String+ or +Hash+.
for the +set-cookie+ header according to RFC6265. The +value+ may be an
Generate an encoded string using the provided +key+ and +value+ suitable
set_cookie_header(key, value) -> encoded string
:call-seq:
def set_cookie_header(key, value) case value when Hash key = escape_cookie_key(key) unless value[:escape_key] == false 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 :same_site value: #{value[:same_site].inspect}" end partitioned = "; partitioned" if value[:partitioned] value = value[:value] else key = escape_cookie_key(key) end value = [value] unless Array === value return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}" end
def set_cookie_header!(headers, key, value)
If the headers already contains a +set-cookie+ key, it will be converted
+value+ using set_cookie_header.
Append a cookie in the specified headers with the given cookie +key+ and
set_cookie_header!(headers, key, value) -> header value
:call-seq:
def set_cookie_header!(headers, key, value) if header = headers[SET_COOKIE] if header.is_a?(Array) header << set_cookie_header(key, value) else headers[SET_COOKIE] = [header, set_cookie_header(key, value)] end else headers[SET_COOKIE] = set_cookie_header(key, value) end end
def status_code(status)
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
def unescape(s, encoding = Encoding::UTF_8)
Unescapes a URI escaped string with +encoding+. +encoding+ will be the
def unescape(s, encoding = Encoding::UTF_8) URI.decode_www_form_component(s, encoding) end
def unescape_path(s)
Unescapes the **path** component of a URI. See Rack::Utils.unescape for
def unescape_path(s) ::URI::DEFAULT_PARSER.unescape s end
def valid_path?(path)
def valid_path?(path) path.valid_encoding? && !path.include?(NULL_BYTE) end