module Rack::Utils
def self.key_space_limit
def self.key_space_limit default_query_parser.key_space_limit end
def self.key_space_limit=(v)
def self.key_space_limit=(v) self.default_query_parser = self.default_query_parser.new_space_limit(v) end
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 add_cookie_to_header(header, key, value)
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] 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 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
def add_remove_cookie_to_header(header, key, value = {})
Adds a cookie that will *remove* a cookie from the client. Hence the
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
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 && 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}[#{escape(k)}]" : escape(k)) }.delete_if(&:empty?).join('&') when nil prefix else raise ArgumentError, "value must be a Hash" if prefix.nil? "#{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, max_ranges: 100)
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, max_ranges: 100) get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges 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!(header, key, value = {})
def delete_cookie_header!(header, key, value = {}) header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value) nil end
def escape(s)
def escape(s) URI.encode_www_form_component(s) end
def escape_html(string)
def escape_html(string) string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] } end
def escape_path(s)
Like URI escaping, but with %20 instead of +. Strictly speaking this is
def escape_path(s) RFC2396_PARSER.escape s end
def get_byte_ranges(http_range, size, max_ranges: 100)
def get_byte_ranges(http_range, size, max_ranges: 100) # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35> 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).inject(0, :+) > size ranges end
def make_delete_cookie_header(header, key, value)
def make_delete_cookie_header(header, key, value) case header when nil, '' cookies = [] when String cookies = header.split("\n") when Array cookies = header end key = escape(key) domain = value[:domain] path = value[:path] regexp = if domain if path /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/ else /\A#{key}=.*domain=#{domain}(?:;|$)/ end elsif path /\A#{key}=.*path=#{path}(?:;|$)/ else /\A#{key}=/ end cookies.reject! { |cookie| regexp.match? cookie } cookies.join("\n") end
def parse_cookies(env)
def parse_cookies(env) parse_cookies_header env[HTTP_COOKIE] end
def parse_cookies_header(header)
def parse_cookies_header(header) # According to RFC 6265: # The syntax for cookie headers only supports semicolons # User Agent -> Server == # Cookie: SID=31d4d96e407aad42; lang=en-US return {} unless header header.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 rfc2109(time)
weekday and month.
Do not use %a and %b from Time.strptime, it would use localized names for
that I'm certain someone implemented only that option.
NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
It assumes that the time is in GMT to comply to the RFC 2109.
of '% %b %Y'.
Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
def rfc2109(time) wday = RFC2822_DAY_NAME[time.wday] mon = RFC2822_MONTH_NAME[time.mon - 1] time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT") 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 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)
To reduce denial of service potential, only the first 16
# => "gzip"
[["compress", 0.5], ["gzip", 1.0]])
select_best_encoding(%w(compress gzip identity),
Example:
Request#accept_encoding.
The accept_encoding argument is typically generated by calling
the highest priority.
priority for the encoding, return the available encoding with
is an encoding name and the second element is the numeric
acceptable encodings array is an array where the first element
acceptable encodings for a request, where each element of the
Given an array of available encoding strings, and an array of
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
def set_cookie_header!(header, key, value)
def set_cookie_header!(header, key, value) header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value) nil end
def status_code(status)
def status_code(status) if status.is_a?(Symbol) SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" } 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) RFC2396_PARSER.unescape s end
def valid_path?(path)
def valid_path?(path) path.valid_encoding? && !path.include?(NULL_BYTE) end