# -*- encoding: binary -*-
# frozen_string_literal: true
require 'uri'
require 'fileutils'
require 'set'
require 'tempfile'
require 'time'
require_relative 'query_parser'
module Rack
# Rack::Utils contains a grab-bag of useful methods for writing web
# applications adopted from all kinds of Ruby libraries.
module Utils
(require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
ParameterTypeError = QueryParser::ParameterTypeError
InvalidParameterError = QueryParser::InvalidParameterError
DEFAULT_SEP = QueryParser::DEFAULT_SEP
COMMON_SEP = QueryParser::COMMON_SEP
KeySpaceConstrainedParams = QueryParser::Params
RFC2822_DAY_NAME = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
RFC2822_MONTH_NAME = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
RFC2396_PARSER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::RFC2396_Parser.new
class << self
attr_accessor :default_query_parser
end
# The default number of bytes to allow parameter keys to take up.
# This helps prevent a rogue client from flooding a Request.
self.default_query_parser = QueryParser.make_default(65536, 100)
module_function
# URI escapes. (CGI style space to +)
def escape(s)
URI.encode_www_form_component(s)
end
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
# true URI escaping.
def escape_path(s)
RFC2396_PARSER.escape s
end
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
# unescaping query parameters or form components.
def unescape_path(s)
RFC2396_PARSER.unescape s
end
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
# target encoding of the string returned, and it defaults to UTF-8
def unescape(s, encoding = Encoding::UTF_8)
URI.decode_www_form_component(s, encoding)
end
class << self
attr_accessor :multipart_total_part_limit
attr_accessor :multipart_file_limit
# multipart_part_limit is the original name of multipart_file_limit, but
# the limit only counts parts with filenames.
alias multipart_part_limit multipart_file_limit
alias multipart_part_limit= multipart_file_limit=
end
# The maximum number of file parts a request can contain. Accepting too
# many parts can lead to the server running out of file handles.
# Set to `0` for no limit.
self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
# The maximum total number of parts a request can contain. Accepting too
# many can lead to excessive memory use and parsing time.
self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
def self.param_depth_limit
default_query_parser.param_depth_limit
end
def self.param_depth_limit=(v)
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
end
def self.key_space_limit
default_query_parser.key_space_limit
end
def self.key_space_limit=(v)
self.default_query_parser = self.default_query_parser.new_space_limit(v)
end
if defined?(Process::CLOCK_MONOTONIC)
def clock_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
else
# :nocov:
def clock_time
Time.now.to_f
end
# :nocov:
end
def parse_query(qs, d = nil, &unescaper)
Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
end
def parse_nested_query(qs, d = nil)
Rack::Utils.default_query_parser.parse_nested_query(qs, d)
end
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 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 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
# 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.
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
ESCAPE_HTML = {
"&" => "&",
"<" => "<",
">" => ">",
"'" => "'",
'"' => """,
"/" => "/"
}
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
# Escape ampersands, brackets and quotes to their HTML/XML entities.
def escape_html(string)
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
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.
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 parse_cookies(env)
parse_cookies_header env[HTTP_COOKIE]
end
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 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 set_cookie_header!(header, key, value)
header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
nil
end
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 delete_cookie_header!(header, key, value = {})
header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
nil
end
# Adds a cookie that will *remove* a cookie from the client. Hence the
# strange method name.
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 rfc2822(time)
time.rfc2822
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.
#
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
# 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.
def byte_ranges(env, size, max_ranges: 100)
get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
end
def get_byte_ranges(http_range, size, max_ranges: 100)
# See